Переклад українською - Арсеній Чеботарьов - Ніжин 2016

Що таке Akka?

Маштабуєма обробка транзакцій в реальному часі

Ми вважаємо, що написання коректного, конкурентного, стійкого до відмов та маштабуємого застосування є дуже складним. Більшість часу це завдяки тому, що ми використовуємо хибні інструменти та хибний рівень абстрайкцій. Akka створений щоб змінити це. Використовуючи Модель Акторів ми підіймаємо рівень абстракції, та провадимо кращу платформу для побудови маштабуємих, стійких та реактивних застосувань — дивіться маніфест Reactive Manifesto щоб отримати додаткові деталі. Для стійкості до відмов ми адаптували модель "нехай ламається", що використовується в індустрії телекомунікацій з великим успіхом для побудови само-виліковних застосувань, та систем, що ніколи не зупиняються. Актори також провадять абстракцію для прозорого розповсюдження та базис для дійсно маштабованих та стійких до відмов застосувань. 

Akka є Open Source та доступна під Apache 2 License.

Завантайжуйте з http://akka.io/downloads.

Будь ласка зауважте, що всі приклади коду компілюються, так що якщо ви бажаєте мати прямий доступ до первинного коду, погляньте на підпроект Akka Docs на github: для Java та Scala.

Akka реалізує унікальний гібрид

Актори

Актори надають вам:

Дивіться главу для Scala або Java.

Стійкість до відмов

Дивіться Стійкість до відмов (Scala) and Стійкість до відмов (Java).

Прозорість до розташування

Все в Akka розоблене для роботи в розподіленому середовищі: всі інтеракції акторів використовують чисту передачу повідомлень, та все є асинхронним. 

Для огляду підтримки кластерів дивіться глави документації  Java та Scala.

Постійність

Повідомлення, отримані актором, опціонально можуть бути постійними, та відтворюватись, коли актор стартує або рестартує. Це дозволяє акторам відтворювати власний стан, навіть якщо JVM зазнає краху, або коли актор мігрує на інший вузол. 

Ви можете знайти більше деталей в відповідній главі для Java або Scala.

Scala та Java API

Akka має Документацію Scala та Документацію Java.

Akka може використовуватись в два різні способа

Akka може використовуватись та розгортатись в різний спосіб:

Чому Akka?

Які можливості може запропонувати Akka у порівнянні з конкурентами?

Akka провадить маштабуєму обробку транзакцій в реальному часі. 

Akka є унифікованим рантаймом та моделлю програмування для:

Це одна річ, що треба вивчити та адмініструвати, з високою згуртованістю та когерентною семантикою.

Akka є дуже маштабованим шматком програмного забезпечення, не тільки в контексті продуктивності, але також в розмірі застосувань, для яких він корисний. Ядро Akka, akka-actor, є дуже малим, та легко вкидається в існуючий проект, де вам треба асинхронність та неблокуючу конкурентність без зайвого галасу.

Ви можете включити тільки частини akka, що потрібіні для вашого застосування. Зі зростанням кількості ядер CPU на кожному циклі, Akka є альтернативою, що провадить визначну продуктівність, навіть якщо виконується на одній машині. Akka також підтримує широке коло парадігм конкурентності, дозволяючи користувачам обрати вірний інструмент для завдання. 

Які гарні приклади застосування Akka?

Ми очікуємо, що Akka буде адаптований багатьма великими організаціями в великому диапазоні індустрій:

та багато інших. Люба система з потребою до високої пропускної спроможності та низької латентності є гарним кандидатом для використання Akka.

Актори дозволяють вам керувати відмовами сервісів (функції супервізора), керування навантаженням (стратегії відступу, таймаути та ізоляція обробки), так само, як горизонтальне та вертикальне маштабування (додають більше ядер та/або додають більше машин).

Ось що деякі користувачі Akka кажуть щодо того, як вони використовують  Akka: http://stackoverflow.com/questions/4493001/good-use-case-for-akka

Починаємо

Попередні вимоги

Akka потребує, щоб ви мали Java 8 або старшу встановленою на вашою машині.

Typesafe провадить коменційний білд Akka та пов'язаних проектів, таких, як Scala або Play, як частину Reactive Platform, що зроблена доступною для Java 6 в разі, якщо ваш проект все ще не може бути перенесений на Java 8. Він також включає додаткові комерційні можливості або бібліотеки.

Вказівники для початківців та шаблони проектів

Найкращий шлях почати вивчати Akka - завантажити Typesafe Activator та спробувати один з шаблонів проекту Akka.

Завантаження

Є декілька шляхів завантажити Akka. Ви можете завантажити його як частину Typesafe Platform (як зазначено вище). Ви можете завантажити повну дистрибуцію, що включає всі модулі. Або ви можете використати інструменти побудови, такі, як Maven або SBT, для завантаження залежностей з Maven репозитарія Akka.

Модулі

Akka є дуже модулярним, та складається з декількох JAR, що містять різні можливості.

  • akka-actor – Касичні актори, типізовані актори, IO актори etc.
  • akka-agent – Агенти, інтегровані зі Scala STM
  • akka-camel – Інтеграція з Apache Camel
  • akka-cluster – Керування кластерами, еластична маршрутизіція
  • akka-osgi – Утілити для використання Akka в контейнерах OSGi
  • akka-osgi-aries – Проект Aries для разервування системів акторів
  • akka-remote – Віддалені актори
  • akka-slf4j – SLF4J Logger (слухач шини подій)
  • akka-testkit – Інструменти для тестування систем акторів

На додаток до ціх стабільних модулів є ще декілька, які на своєму шляху в стабільне ядро, але наразі все ще марковані як "експерементальні". Це не означає, що вони не функціонують як задумані. Це, в основному, означає, що їх API ще не достатньо сталі, щоб вважатись замороженими. Ви можете допомогти прискорити цей процес, надаючи зворотній зв'язок по цім модулям в список розсилки. 

  • akka-contrib – різноманітні контрибуції, що можуть, або ні, переміститись до головних модулей, дивіться Експериментальні контрибуції для додаткових деталей.

Ім'я файлу дійсного JAR, наприклад, akka-actor_2.11-2.4.1.jar (та аналогічно для інших модулей).

Побачити залежності JAR для кожного модуля Akka можна в розділі Залежності.

Використання реліз дистрибутиву

Завантажте потрібний реліз з http://akka.io/downloads та розпакуйте його.

Використання версії снепшоту

Нічні снепшоти Akka публікуються на http://repo.akka.io/snapshots/ та мають версії з обома, SNAPSHOT та маркерами часу. Ви можете обрати версію з маркером часу, якщо ви бажаєте робити з ними, та можете потім вирішити оновитись до новішої версії.

Попередження

Використання Akka SNAPSHOT, нічних та віхових релізів не заохочується, за винятком випадку коли ви знаєте, що робите.

Використання інструментів побудови

Akka може використовуватись з інструментами побудови, що підтримують репозитарії Maven.

Репозитарії Maven

Для версії Akka 2.1-M2 та далі:

Maven Central

Для попередніх версій Akka:

Akka Repo

Використання  Akka з Maven

Простіший шлях, щоб почати з Akka та Maven є звернутись до підручника Typesafe Activator з назвою Akka Main in Java.

Оскільки  Akka опублікований на Maven Central (для версій починаючи з 2.1-M2), достатньо додати залежності Akka до POM. Наприклад, ось залежності для akka-actor:


  1. com.typesafe.akka
  2. akka-actor_2.11
  3. 2.4.1

Для версій снепшоту також повинна бути додана залежність до репозирарію снепшоту:


  1. akka-snapshots
  2. true
  3. http://repo.akka.io/snapshots/

Зауваження: версії снепшотів публікуються як  SNAPSHOT та версії з маркерами часу.

Використання Akka з SBT

Простіший шлях почати з Akka та SBT є завантажити шаблон проекту Akka/SBT.

Підсумок базових частин для використання Akka з SBT:

Інструкції інсталяції SBT на https://github.com/harrah/xsbt/wiki/Setup

build.sbt файл:

  1. name := "My Project"
  2.  
  3. version := "1.0"
  4.  
  5. scalaVersion := "2.11.7"
  6.  
  7. libraryDependencies +=
  8. "com.typesafe.akka" %% "akka-actor" % "2.4.1"

Зауваження: налаштування libraryDependencies, надані вище, специфічні до SBT v0.12.x та вище. Якщо ви використовуєте старішу версію SBT, libraryDependencies має виглядати наступним чином:

  1. libraryDependencies +=
  2. "com.typesafe.akka" % "akka-actor_2.11" % "2.4.1"

Для спепшот версій потрібно також додати репозитарій снепшоту:

  1. resolvers += "Akka Snapshot Repository" at "http://repo.akka.io/snapshots/"

Використання  Akka з Gradle

Потребує щонайменьше Gradle 1.4 з використанням Scala plugin

  1. apply plugin: 'scala'
  2.  
  3. repositories {
  4. mavenCentral()
  5. }
  6.  
  7. dependencies {
  8. compile 'org.scala-lang:scala-library:2.11.7'
  9. }
  10.  
  11. tasks.withType(ScalaCompile) {
  12. scalaCompileOptions.useAnt = false
  13. }
  14.  
  15. dependencies {
  16. compile group: 'com.typesafe.akka', name: 'akka-actor_2.11', version: '2.4.1'
  17. compile group: 'org.scala-lang', name: 'scala-library', version: '2.11.7'
  18. }

Для версій снепшоту також треба додати репозитарій снепшотів:

  1. repositories {
  2. mavenCentral()
  3. maven {
  4. url "http://repo.akka.io/snapshots/"
  5. }
  6. }

Використання Akka з Eclipse

Встановіть проект SBT та потім використовуйте sbteclipse для генерації проекту Eclipse.

Використання  Akka з IntelliJ IDEA

Встановіть проект SBT та потім використовуйте sbt-idea для генерації проекту IntelliJ IDEA.

Використання  Akka з NetBeans

Встановіть проект SBT  та потім використовуйте nbsbt  для генерації проекту NetBeans.

Ви можете також використовувати nbscala для загальної підтримки scala в IDE.

Не використовуйте флаг Scala компілятора -optimize

Попередження

Akka не був скомпільований або протестований з флагом Scala компілятора -optimize. Є повідомлення від користувачів, що спробували його використання, про дивну поведінку. 

Побудова з джерельних текстів

Akka використовує Git та розташований на Github.

Продовжіть читання на сторінці Building Akka

Потрібна допомога?

Якщо ви маєте запитання, ви можете отримати допомогу в Akka Mailing List.

Ви можете також запросити комерційну підтримку.

Дякуємо, що ви є частиною спільноти Akka.

Обов'язковий Hello World

Версія на основі акторів для складної проблеми друку добре відомого привітання на консоль представлена в підручнику Typesafe Activator в розділі Akka Main in Scala.

Інструкція ілюструє головний клас запуску akka.Main, що очікує тільки один аргумент командного рядка: ім'я класу головного актора застосування. Цей головний метод буде створювати інфраструктуру, потрібну для виконання акторів, запускає наданий головний актор, та керує цілим застосуванням, щоб завершитись, коли завершується головний актор. 

Є також інша інструкція Typesafe Activator з того ж розділу знань, що називається Hello Akka!. Він описує основи Akka більш глибоко.

Застосування та сценарії розгортання

Як я можу застосовувати та розгортати Akka?

Akka може бути використаний в різний спосіб:

  • Як бібліотека: використовуватись як звичайний JAR на classpath та/або в веб застосунку, та розташовуватись в WEB-INF/lib
  • Пакується за допомогою sbt-native-packager
  • Пакується та розгортається з використанням Typesafe ConductR.

Природний пакувальник

sbt-native-packager є інструментом для створення дистрибутивів любого типу застосувань, всключи Akka застосування.

Визначте версію sbt в файлі project/build.properties:

  1. sbt.version=0.13.7

Додайте sbt-native-packager в файл project/plugins.sbt:

  1. addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.0.0-RC1")

Використовуйте налаштування пакунку та опціонально вкажіть mainClass в файлі build.sbt:

  1. import NativePackagerHelper._
  2.  
  3. name := "akka-sample-main-scala"
  4.  
  5. version := "2.4.1"
  6.  
  7. scalaVersion := "2.11.7"
  8.  
  9. libraryDependencies ++= Seq(
  10. "com.typesafe.akka" %% "akka-actor" % "2.4.1"
  11. )
  12.  
  13. enablePlugins(JavaServerAppPackaging)
  14.  
  15. mainClass in Compile := Some("sample.hello.Main")
  16.  
  17. mappings in Universal ++= {
  18. // опціональний приклад, що ілюструє копіювання додаткового каталогу
  19. directory("scripts") ++
  20. // копіювання файлів конфігурації до каталогу конфігурації
  21. contentOf("src/main/resources").toMap.mapValues("config/" + _)
  22. }
  23.  
  24. // додати каталог 'config' до classpath стартового скрипту,
  25. // альтернативно встановити розміщення файлу конфігурації через параметри CLI
  26. // при запуску застосування
  27. scriptClasspath := Seq("../config/") ++ scriptClasspath.value

Зауваження

Використовуйте JavaServerAppPackaging. Не використовуйте  AkkaAppPackaging (що раніше називався packageArchetype.akka_application, оскільки він не має тієї ж гнучкості та якості, як JavaServerAppPackaging.

Використовання завдання sbt  dist запакує застосування.

Щоб розпочати застосування (на unix подібній системі):

  1. cd target/universal/
  2. unzip akka-sample-main-scala-2.4.1.zip
  3. chmod u+x akka-sample-main-scala-2.4.1/bin/akka-sample-main-scala
  4. akka-sample-main-scala-2.4.1/bin/akka-sample-main-scala sample.hello.Main

Використовуйте Ctrl-C щоб перервати та вийти з застосування.

На Windows машині ви можете також використати скриптbin\akka-sample-main-scala.bat.

Приклади застосувань Akka

Ми сподіваємось, що Akka буде адаптовано багатьма великими організаціями в великому диапазоні індустрій, від інвестицій та банківської справи, продажів та соціальних медіа, ігорного бізнесу та ставок, систем автомобільного трафіку, охорони здоров'я та багато іншого. Люба система з потребою до високої пропускної спроможності та низької латентності є гарним кандидатом для використання Akka.

Точиться велика дискуссія щодо застосування Akka з деякими гарними промовами від промислових користувачів тут

Ось деякі галузі, де було розвернуто Akka в промисловому оточенні

Обробка транзакцій (онлайн геймінг, фінанси/банкінг, торги, статистика, ставки, соціальні медіа, телеком)

Маштабування вгору, маштабування вширину, стійкість до відмов, висока доступність

Основа для сервісів (будь яка індустрія, будь яке застосування)

Сервіси REST, SOAP, Cometd, WebSockets etc. Діє як хаб/рівень інтеграції. Маштабування вгору, маштабування вширину, стійкість до відмов, висока доступність

Конкурентність/паралелізм (любе застосування)

Коректно, просто до застосування та розуміння, просте додавання jar до існуючого проекту (використання Scala, Java, Groovy, або JRuby)

Симуляція

Мастер/робітник, обчислювальні сітки, MapReduce etc.

Пакетна обробка (люба індустрія)

Інтеграція з Camel для перехоплення пакетних джерел даних, актори поділяюють пакетне навантаження

Сфера телекомунікацій (телеком, веб медіа, мобільні медіа)

Маштабування вгору, маштабування вширину, стійкість до відмов, висока доступність

Гральна індустрія та ставки (MOM, онлайн ігри, ставки)

Маштабування вгору, маштабування вширину, стійкість до відмов, висока доступність

Бізнес логіка/добування даних/обробники загального призначення

Маштабування вгору, маштабування вширину, стійкість до відмов, висока доступність

Обробка складних потоків подій

Маштабування вгору, маштабування вширину, стійкість до відмов, висока доступність

Термінологія, концепції

В цій главі ми спробуємо встановити загальну термінологію для визначення солідного підгрунтя комунікації щодо конкурентних, розподілених систем, на які націлений Akka. Будь ласка, майте на увазі, що для більшості з ціх термінів немає загально узгодженного визначення. Ми просто бажаємо знайти робочі визначення, що будуть використовуватись в межах документації Akka.

Конкурентність  vs. паралелізм

Конкурентність та паралелізм є пов'язаними концепціями, але є невеликі розбіжності. Конкурентність означає, що два або більше завдань просуваються вперед, навіть якщо вони не можуть виконуватись одночасно. Це може бути досягнено, наприклад, за допомогою кватнів часу, коли частини завдань виконуюються послідовно, та перемішуються з частинами інших завдань. Паралелізм, з іншого боку, виникає, коли виконання дійсно може відбуватись одночасно.

Асинхронність  vs. синхронність

Виклик методу вважається синхронним, якщо викликач не може просуватись, доки метод не поверне значення, або викличе виключення. З іншого боку, асинхронний виклик дозволяє викликачу просуватись далі на обмежену кількість кроків, та про завершення методу може бути повідомлено через додатковий механізм (це може бути зареєстрований зворотній виклик, Future, або повідомлення).

Синхронне API може використовувати блокування для реалізації синхронності, але це не обов'язково. Кожне інтенсивне до CPU завдання може надавати таку ж поведінку, як і блокування. Взагалі, перевагу мають асинхронні API, тому що вони гарантують, що система може просуватись. Актори є асинхронними по природі: актор може просуватись після відправки повідомлення, без очікування коли насправді відбудеться доставка.

Неблокуючі vs. блокуючі

Ми кажемо про блокування, якщо затримка одного потоку може невизначено затримати деякі інші потоки. Гарним прикладом є ресурс, який може бути використаний ексклюзивно одним потоком з використанням взаємного виключення. Якщо потік утримує ресурс невизначений час (наприклад, випадково увійшовши в безскінчений цикл), інші потоки, що очікуватимуть ресурс, не будуть просуватись. Навпаки, неблокуючий, означає, що ніякий потік не може затримати інший на невизначений час. 

Неблокуючі операції мають перевагу над блокуючими, тому що загальний прогрес системи тривіально не гарантований, коли вона містить блокуючі операції.

Глухий кут (deadlock) vs. голодування (starvation) vs. живий кут (live-lock)

Глухий кут виникає, коли декілька учасників очікують один одного, щоб досягти визначеного стану, в якому можна продовжувати далі. Жодний з них не може просуватись, без того, щоб інший не досяг визначеного стану (проблема "Catch-22"), що призводить до зупинки всіх підсистем. Глухий кут близько пов'язаний з блокуванням, тому що необхідною умовою є те, щоб потік міг затримати просування інших потоків на невезначений час.

На відміну випадку глухого кута, коли учасники не можуть просуватись, під час голодування учасники можуть прогресувати, але один або декілька - ні. Типовий сценарій це випадок, коли діє природний алгоритм планування, що обирає високоприоритетні завдання, скоріше, ніж, низькоприоритетні. Якщо число надходящих високоприоритетних завдань постійне і досить велике, низькоприоритетні можуть ніколи не завершитись. 

Живий кут подібний до глухого кута, бо жодний з учасників не може прогресувати. Різниця, однак, полягає в тому, що замість бути замороженими в стані очікування, коли інші просунуться, учасники постійно змінюють свій стан. Приклад цього сценарію, коли два учасника мають два доступні ідентичні ресурси. Кожний з них намагається отримати ресурс, але вони також перевіряють, чи інший також потребує ресурс. Коли ресурс запитаний іншим, вони намагаються отримати інший примірник ресурсу. В несчасливому випадку може статись, коли два учасника "скачуть" між двома ресурсами, ніколи не захвачуючи їх, але кожного разу поступаючись один одному.

Стан перегонів

Ми називаємо це станом перегонів, коли припущення щодо впорядкованості набору подій може бути зруйновані зовнішніми недермінованими ефектами. Стан перегонів часто виникають, коли багато потоків мають загальний змінний стан, та операції в потоці відповідно цього стану можуть перемішуватись неочікуваним чином. Хоча це загальний випадок, загальний стан не обов'язково призводить до стану перегонів. Одним з прикладів може бути клієнт, що надсилає невпорядковані пакети (наприклад, датаграми UDP) P1, P2 до сервера. Як пакети можуть потенційно мандрувати через різні мережеві маршрути, існує можливість, що отримає P2 раніше, та  P1 після цього. Якщо повідомлення не містять інфорамаці щодо порядку їх надсилання, сервер не має можливості визначити, що вони надіслані в іншому порядку. В залежності від значення пакетів, це може спричинити стан перегонів.

Зауваження

Єдиною гарантією, що провадить Akka щодо повідомлень, відісланних між даною парою акторів, є те, що їх порядок завжди зберігаєтсья. Дивіться Надійність доставки повідомлень

Гарантування відсутності блокування (умови прогресу)

Як обговорювалось в попередніх розділах, блокування є небажаним з декількох причин, включаючи загрозу мертвого куту, та зменшеної пропускної спроможності системи. В наступному розділі ми обсудимо різноманітні неблокуючі властивості з різними потужностями. 

Вільність від очікування (Wait freedom)

Метод є вільним від очікування, якщо кожний виклик гарантовано завершиться за скінчене число кроків. Якщо метод визначено вільний від очікування, число кроків має скінчену межу. 

З цього визначення слідує, що вільні від очікування методи ніколи не блокуються, і, таким чином, глухий кут не виникає. Додатково, позаяк кожний приймаючий участь може прогресувати після скінченого числа кроків (коли виклик завершиться), вільні від очікування методи вільні від голодування.

Вільність від заморожування (Lock freedom)

Вільність від заморожування є слабкішою властивістю, ніж вільність від очікування. В цьому випадку безскінчено часто деякі методи завершуються за скінчене число кроків. Це визначення припускає, що глухий кут неможливий для вільних від заморожування викликів. З іншого боку, гарантії, що деякі виклики завершуються в скінчене число кроків не є достатньою гарантією, що всі з них колись завершаться. Іншими словами, вільність від заморожування не є достатньою гарантією від голодування.

Вільність від захаращення (Obstruction-freedom)

Вільність від захаращення є найслабшою неблокуючою гарантією, що обговорюється тут. Метод називається вільним від захаращення, якщо є точка в часі, після якої він виконується в ізоляції (інші потоки не роблять кроків, тобто знаходяться в призупиненному стані), він завершується в обмежене число кроків. Всі об'єкти, вільні від заморожування, також захищені від захаращення, але протилежне загалом невірно. 

Оптимістичне управління конкурентністю (Optimistic concurrency control, OOC) часто також вільне від захаращення. Підхід OCC полягає в тому, що кожний учасник намагається виконати свою операцію з розділеним об'єктом, але якщо він визначає конфлікти з іншими, він відкатує модифікації, та намагається знову згідно деякого розкладу. Якщо настає точка в часі, коли один з учасників є єдиним, хто родить спроби, операція буде успішною.

Системи акторів

Актори - це об'єкти, що інкапсулюють стан та поведінку, вони комунікують виключно через обмін повідомленнями, що розміщуються в поштовій скринці отримувача. В цьому сенсі актори є найбільш прямою формою об'єктно-орієнтовного програмування, але краще дивитись на них як на особистості: коли рішення моделюється за допомогою акторів, уявляйте їх як групу людей, та давайте їм суб-завдання, розташовуйте їх функції в організаційну структуру та думайте про те, як подолати відмову (все це з вигодами не мати справу з дійсними людьми, що означає не мати потреби звертати увагу на власний емоційний стан або моральні вади). Результат може прислужитись як моральна підтримка для побудови програмної реалізації.

Зауваження

ActorSystem є важкою структурою, що розміщує 1…N потоків, так що створюйте один примірник для кожного логічного застосування.

Ієрархічна структура

Подібно до економічної організації, актори природно формують ієрархії. Один актор, що наглядає за окремою функцією в програмі, може вирішити розділити свої завдання на меньші, більш керовані шматки. Для ціх цілей він стартує дочірні актори, за якими він наглядатиме. Хоча деталі супервізора пояснені тут, ми сконцентруємося на підлеглих концепціях в цьому розділі. Єдина передумова - це знати, що кожний актор має тільки один супервізор, що також є актором, який його створив. 

Квінтессенція систем акторів в ціх завданнях, що поділяються та делегуються нижче, доки вони не стануть досить малими, щоб бути обробленими в одму місці. Таким чином не тільки самі завдання стають більш зрозуміло структурованими, але також отримані актори можуть бути визначені в термінах повідомлень, які вони мають обробляти, як вони мають звичайно реагувати, та як повинні оброблятись відмови. Якщо один актор не має змоги для подолання окремої ситуації, він надсилає відповідне повідомлення про відказ супервізору, покликаючи на допомогу. Рекурсивна структура дозволяє обробити відказ на вищому рівні. 

Порівняйте це з розшарованим дизайном програмного забезпечення, що легко скочується до захисного програмування, ціллю якого є не випустити назовні жодного відказу: якщо проблема доведена до вірної особи, кращим рішенням може бути змога тримати все “під ковдрою”.

Тепер складність розробки такої системи в тому, як вирішити, хто повинен наглядати за усім. Зрозуміло, що немає єдиного найкращого рішення, але є декілька думок, що можуть бути корисними:

  • Якщо один актор керує тим, що робить інший актор, надсилаючи суб-завдання, тоді менеджер повинен наглядати за дочірнім актором. Причиною є те, що менеджер знає, які типи збоїв очікуються, та як обробляти їх. 
  • Якщо один актор переносить дуже важливі дані (тобто його стан не можна втратити, якщо цього можна уникнути), цей актор повинен віддавати любі можливі загрозливі суб-завдання дочкам, яких він наглядає, та обробляти їх збої відповідно. В залежності від природи запитів, може бути кращим створити нового актора для кожного запиту, що спрощує керування станом для збору відповідей. Це відоме як  “Шаблон ядра помилок” (Error Kernel Pattern) з Erlang.
  • Якщо актор залежить від іншого актора, на якого він переклав своє навантаження, він повинен наглядати, щоб той актор був живий та діяв до отримання завершального повідомлення. Це відрізняється від супервізора, тому що нагляд частково не має впливу на стратегію супервізора. Також слід зауважити, що окремо сама функціональна залежність не є критерієм для вирішення, де саме розташувати окремого актора в ієрархії.

Звичайно, завжди є виключення з ціх правил, але не важливо, чи ви слідуєте правилам, чи ви їх порушуєте - в жодному разі ви матиметиме на це привід.

Контейнер конфігурації

Система акторів, як асамблея колаборації акторів, є природним пристроєм для керування розподіленими застосунками, як сервіси планування, конфігурації, журналювання, etc. Декілька систем акторів з різними конфігураціями можуть співіснувати в тій же JVM без проблем, немає глобального розподіленого стану в самомоу Akka. Поєднайте це з прозорою комунікацією між системами акторів — на одному вузлі або через мережеве з'єднання — щоб побачити, що системи акторів самі по собі можуть використовуватись як будівельні блоки в функціональній ієрархії.

Як мають поводитись актори

  1. Актори повинні бути гарними співробітниками: виконувати свою роботу ефективно, не турбуючи будь-кого іншого без потреби, та запобігати спотворення ресурсів. Перекладаючи на програмування це означає обробляти події те генрувати відповіді (або інші запити) в подія-рушійній манері. Актори не повинні блокувати (тобто пасивно очікувати, займаючи потік) на деяких зовнішні сутностях — що може бути блокування, мережевий сокет, таке інше — за винятком коли цього не можна уникнути; в останньому випадку дивіться нижче.
  2. Не передавати змінні об'єкти між акторами. Щоб забезпечити це надавайте перевагу незмінним повідомленням. Якщо інкапсуляція акторів зруйнована через викриття іх змінного стану назовні, ви приходите до земель звичайної конкурентності Java, з усіпа недоліками.
  3. Актори зроблені бути контейнерами для стану і поведінки, що означає не надсилати поведінку в повідомленнях (що може бути спокусливим з використанням замикань Scala). Один з ризиків є ненавмисне поділення змінного стану між акторами, та це порушення моделі акторів нажаль руйнує всі властивості, що роблять програмування з акторами таким гарним досвідом. 
  4. Актори вищого рівню є внутрішньою частиною вашого ядра помилок, так що створюйте їх помірковано, та схиляйтесь до справжні ієрархічних систем. Це має переваги з точки зору обробки збоїв (з обох боків, зважаючи на гранулярність конфігурації та продуктивності), та це також зменьшує навантаження на актора-охоронця, що є єдиною точкою спору в разі надмірного використання.

Блокування потребує уважного менеджменту

В деяких випадках не можна уникнути виконання блокуючої операції, тобто відправити потік в сон на невизначений термін, очікуючи виникнення зовнішньої події. Прикладами є старі драйвери RDBMS або API повідомлень, та грунтовна причина типово така, що в основі лежить (мережевий) ввод-вивод. Коли ви стикиєтесь з цім, ми можете бути спокушені прото огорнути блокуючий виклик в Future та надалі робити з ним, але ця стратегія дуже проста: ви, напевне, знайдете тонкі місця або вичерпаєте ліміт пам'яті або потоків, коли застосування виконуватиметься під збільшенним навантаженням. 

Неповний перелік адекватних рішень “блокуючої проблеми” включає наступні поради:

  • Робіть блокуючий виклик в акторі (або в наборі акторів, що керуються маршрутизатором), переконавшись, що сконфігуровано пул потоків, що або призначені для цієї цілі, або мають достатній розмір. 
  • Робіть блокуючий виклик в Future, провадячи вищий ліміт числа таких викликівв кожний момент часу (надання необмеженого числа завдань такої природи вичерпає запаси вашої пам'яті та потоків).
  • Робіть блокуючий виклик в Future, провадячи пул потоків з обмеженням числа потоків, що відповідає апаратному забезпеченні, на якому виконується застосування.
  • Виділіть один потік для управління набору блокуючих ресурсів (токий як NIO селектор, що рухає декілька каналів) та відправляє події по мірі виникнення як повідомлення акторів.

Перша можливість особливо гарно підходить до ресурсів, що однопоточні по своїй натурі, як указівники на бази даних, що традиційно можуть одночасно виконувати тільки один окремий запит, та використовують внутрішню синхронізацію для забезпечення цього. Загальний шаблон є створення маршрутизатора для N акторів, кожний з яких огортає одне з'єднання з DB та обробляє запити, надіслані до маршрутизатора. Потім число N мусить бути налаштовано для найбільшої полоси пропускання, що буде варіювати в залежності від того, яка DBMS розгорнута, та на якому обладнанні.

Зауваження

Конфігурація пулів потоків я завданням, що краще делегувати Akka, просто сконфігурувавши в application.conf та реалізувати через ActorSystem

Про що вам не треба турбуватись

Система акторів керує ресурсами, що сконфігуровані для використання, щоб виконувати акторів, що вона містить. Може бути мільйони акторів в одній такій системі, та по всьому мантра в тому, щоб бачити їх як ряску з накладними розходами приблизно 300 байт на примірник. Природно, що точний порядок, в якому повідомлення оброляються в великих системах не може конролюватись автором застосування, але цього і не було в намірах. Відступіть та заспокойтесь, доки Akka візьме важку роботу на себе.

Що таке актор?

Попередний розділ щодо Систем акторів пояснив, як актори формують ієрархії та є найменьшими блоками при побудові застосувань. Цей розділ розглядає один такий актор в ізоляції, пояснюючи концепції, які стануть вам в нагоді, коли ви будете реалізовувати їх. it. Для більш глибокого посилання з усіма деталями звертайтесь до Актори (Scala) та Нетиповані актори (Java).

Актор є контейнером для стану, поведінки, поштової скриньки, дітей та стратегії супервізора. Все це інкапсюловано за Посиланням на актора (Actor Reference). Нарешті, треба розглянути, що відбувається Коли актор зевершує роботу.

Посилання на актора

Як деталізується нижче, об'єкт актора повинен бути захищений від зовнішнього світу, щоб отримати переваги від моделі акторів. Таким чином актори представлені загалу з використанням посилань, що є об'єктами, що можуть бути вільно передані та без обмежень. Цей поділ на внутрішній та зовнішній об'єкти дозволяє прозорість для всіх бажаних операцій: рестарт актора без потреби оновлення посилання будь-де, розташування справжнього актора на віддалених вузлах, надсилання повідомлень акторам з повністю різних застосувань. Але найбільш важливий аспект в тому, що неможливо подивитись на нутрощі актора та отримати його стан ззовні, якщо тільки актор нерозсудливо не опублікує цю інформацію самостійно.

Стан

Об'єкти акторів будуть типово містити деякі змінні, які відображують можливі стани актора, що можуть виникнути. Це може бути машина станів в яіному вигляді (тобто з використанням модуля FSM), або це може бути лічильник, встановлені на слухачів, очікувані запити, таке інше. Це те, що робить акторів значущим, та вони мають бути захищені від пошкодження іншими акторами. Гарна новина полягає в тому, що актори Akka концептуально кожний має свій легковажний потік, що повністю закрите від решти системи. Це означає, що замість мати синхронизований доступ з використанням блокувань, ви можете просто писати ваш код акторів, взагалі без хвилювання щодо конкурентності.

За лаштунками сцени Akka буде виконувати набори акторів на наборах справжніх потоків, де зазвичай декілька акторів поділяють один потік, та послідовні виклики одного актора можуть призвести до обробки на різних потоках. Akka забезпечує, що ці деталі реалізації не впливають на однопоточність обробки стану актора.

Оскільки внутрішній стан є життєдайним для роботи актора, неузгодженний стан є фатальним. Таким чином, коли актор схиблює та супервізор рестартує його, стан буде створений з початку, як під час першого створення актора. Це утворює можливість самолікування системи. 

Опціонально, стан актора может бути автоматично відтворенний до стану перед рестартом, записуючи отримані повідомлення та програваючи їх перед рестартом (дивіться Постійність).

Поведінка

Кожного разу, коли обробляється повідомлення, воно порівнюється з поточною поведінкою актора. Поведінка означає функцію, що означає дії, що виконуються як реакція на подію в данний проміжок часу. Скажімо "переслати запит, якщо підлеглий авторизований, та відмовити інакше". Ця поведінка може змінюватись з часом, оскільки різні клієнти отримують авторизацію з часом, або тому що актор може перейти в стан "не обслуговується", та потім повернутись. Ці зміни досягаються або кодуванням їх в змінних стану, що читаються в логиці поведінки, або в самій функції, що може замінюватись під час виконання, дивіться операції become та unbecome. Однак початкова поведінка, визначена під час конструювання об'єкта актора, особлива в тому сенсі, що рестарт актора буде скидати поведінку в цей первинний стан. 

Поштова скринька

Призначення актора полягає в обробці повідомлень, та ці повідомлення надсилаються до актора від іншого актора (або з за меж системи акторів). Місце, що поєднує відправника та отримувача є поштовою скринькою актора: кожний актор має тільки одну поштову скриньку, куди всі відправники ставлять в чергу свої повідомлення. Постановка в чергу відбувається в порядку надсилання операцій, що означає, що повідомлення, надіслані від різних акторів, не може мати різний порядок під час виконання через наочну випадковість розподілення акторів між потоками. Надсилання декількох повідомлень до тієї ж цілі від того ж актора, з іншого боку, буде ставити їх в чергу в тому ж порядку. 

Є різні реалізації поштових скриньок, з яких є вибір. По замовчанню встановлена поштова скринька типу FIFO: порядок повідомлень, що обробляється актором, співпадає з порядком як вони ставились в чергу. Це зазвичай гарно по замовчанню, але застосування можуть потребувати надати приоритет деяким повідомленням, порівняно з іншими. В цьому випадку скринька з приоритетами буде ставити в чергу не обов'язково в кінець, але в позицію, що диктується приоритетом повідомлення, можливо навіть на початок черги. При використанні такої черги порядок повідомлень визначається алгоритмом черги, та, загалом, не буде FIFO.

Важлива можливість, якою Akka відрізняється від інших реалізацій акторів, полягає в тому, що поточна поведінка повинна завжди обробляти наступне повідомлення з черги, немає сканування поштової скриньки в пошуках наступного співпадаючого повідомлення. Збій при обробці повідомлення типово буде саме збій, якщо ця поведінка не була перевизначена. 

Діти

Кожний актор потенційно є супервізором: якщо він створює дочірні актори для делегації суб-завдання, він буде автоматично наглядати за ними. Перелік дітей підтримується в контексті актора, та актор має доступ до нього. Модифікації списку виконуються створенням  (context.actorOf(...)) або зупинкою (context.stop(child)) дітей, та ці дії відтворюються безпосередньо. Справжні дії створення та завершення відбуваються за лаштунками сцени, асинхронним чином, так що вони не "блокують" ваш супервізор. 

Стратегія супервізора

Остання частина актора - ща стратегія обробки збоїв його дітей. Обробка збоїв потім виконується прозоро в Akka, застосовуючи одну зі стратегій, описаних в Нагляд та моніторинг для кожного вхідного збою. Позаяк ця стратегія є фундаментальною до того, як структурована система акторів, вона не може бути змінена після створення актора. 

Приймаючи до уваги, що є тільки одна така ятратегія для кожного актора, це означає, що коли різні стратегії застосовуються для різних дітей актора, діти повинні бути згруповані по проміжних супервізорах з відповідними стратегіями, вважаючи за краще ще раз структурувати системи акторів згідно до розподілення завдань на суб-завдання. 

Коли актор завершується

Коли актор завершується, тобто дає збій в такий шлях, що не оброблюється рестартом, завершує себе або зупиняється супервізором, він вивільняє всі ресурси, скидає всі повідомлення, що залишились в поштовій скринці, в системну “скриньку мертіих повідомлень”, що буде передана до EventStream як DeadLetters. Потім поштова скринька замінюється в посиланні на актора на системну поштову скриньку, переводячи всі нові повідомлення до EventStream як DeadLetters. Це, однак, зроблено на основі кращого з можливого, так що не розраховуйте на це, щоб конструювати “гарантовану доставку”.

Причина не просто мовчки складати повідомлення підказана нашими тестами: ми реєстрували TestEventListener на шині подій, до якої надсилались мертві повідомлення, та він журналював попередження для кожного отриманого мертвого повідомлення — це було дуже корисно для дешифрування тестових збоїв більш швидко. Вважається, що ця можливість може також використовуватись для інших цілей. 

Нагляд та моніторинг

Ця глава описує концепції та підгрунтя нагляду (супервізора), запропонованих примітивів, та їх семантик. Для детального опису, як це транслюється в реальний код, звертайтесь до відповідних глав Scala та Java API.

Що значить нагляд супервізора

Як описано в Системах акторів нагляд визначає відносини залежності між акторами: супервізор делегує завдання підлеглим, і, таким чином, має відповідати на їх збої. Коли підлеглий визначає помилку (тобто викликає виключення), він призупиняє себе та своїх підлеглих, та відсилає повідомлення своєму наглядачу, сповіщающи про збій. В залежності від природи збою супервізор має вибір з наступних чотирьох опцій:

  1. Відновити підлеглого, зберігаючи його акумульований внутрішній стан
  2. Рестартувати підлеглого, очищуючи його акумульований внутрішній стан
  3. Зупинити підлеглого назавжди
  4. Ескалувати збій, тобто самому об'явити збій

Важливо завжди дивитись на актора як на частину ієрархії супервізоров, що пояснює існування четвертого вибору (позаяк супервізор також підлеглий для іншого супервізора рівнем вище), та має вплив на перші три: відновлення роботи актора відновлює всіх його підлеглих, рестарт актора рестартує всіх його підлеглих (але дивіться нижче щодо деталей), подібно завершення актора також завершує всіх його підлеглих. Треба зазначити, що по замовчанню поведінка перехоплювача preRestart класу Actor є завершення всіх дітей перед рестартом, але цей перехоплювач може бути перевизначений; рекурсивний рестарт стосується до всіх дітей, що залишились після того, як цей перехоплювач буде виконаний.

Кожний супервізор сконфігуровано за допомогою функції, що транслює всі можливі причини збоїв (тобто виключення) в один з чотирьох наданих вище виборів; важливо, що ця функція не приймає ідентифікатор збійного актора як вхідний параметр. Досить просто навести приклади структур, де це може не виглядати досить гнучким, тобто бажане мати різні стратегії стосовно різних підлеглих. На цей час життєво важно зрозуміти, що нагляд призначений для формування рекурсивної структури обробки збоїв. Якщо ви спробуєте зробити дуже багато на одному рівні, це буде складним пояснити, оскільки рекомендованим шляхом в цьому випадку додати рівень нагляду.

Akka реалізує специфічну форму з назвою “батьківський нагляд”. Актори можуть бути створені тільки іншими акторами — де актор вищого рівня провадиться бібліотекою  — та кожний створений актор наглядається його батьківстким актором. Це обмеження робить формування ієрархії акторів-супервізоров неявним, та заохочує вражаючі рішення. Треба зазначити, що це також гарантує, що актори не можуть загубитись, або бути приєднані до супервізорів ззовні, що могло в іншому випадку захопити їх зненацька. На додаток, це In addition, це дає природну та чисту процедуру завершення для (суб-дерев) застосувань акторів.

Попередження

Пов'язані з наглядом комунікації батько-дитина відбуваються за допомогою спеціальної системи повідомлень, що мають свої власні поштові скриньки, відмінні від користувацьких повідомлень. Це передбачає, що події, пов'язані з наглядом, не є детерміновано впорядкованими відносно звичайних повідомлень. Загалом користувач не може впливати на порядок звичайних повідомлень та повідомлень про збої. Щодо деталей та приклада дивіться розділ Дискусія: порядок повідомлень.

Високорівневі супервізори

../_images/guardians.png

Система акторів буде під час створення стартувати щонайменьше три актори, показані на малюнку вище. Для додаткової інформації щодо  наслідків для шляхів акторів дивіться Сфери вищого рівня для шляхів акторів

/user: Актор-вартівник

Актор, з яким напевне відбувається найбільше взаємодій, є батьківський для всіх користувацьких акторів, вартівник на ім'я "/user". Актори, створені з використанням system.actorOf(), є діти цього актора. Це означає, що коли цей вартівник завершується, всі нормальні актори в системі також завершуються. Це також означає, що стратегія супервізора вартівника визначає, як відбувається нагляд за високорівневими нормальними акторами. Починаючи з Akka 2.1 можливо сконфігурувати це, використовуючи налаштування akka.actor.guardian-supervisor-strategy, що сприймає повністю кваліфіковане ім'я класу SupervisorStrategyConfigurator. Коли вартівник ескалує збій, відповідь кореневого вартівника буде завершити вартівника, що ефективно завершить цілу систему акторів. 

/system: Системний вартівник

Спеціальний вартівник був введений, щоб досягти впорядкованої послідовністі завершення, коли журналюванню залишається активною, тоді як всі нормальні актори завершуються, навіть вважаючи, що журнал самий реалізовано з використанням акторів. Це втілено за допомогоюсистемного вартівника, що наглядає за користувацьким наглядачем, та ініціює своє власне завершення при отриманні повідомлення

Terminated. Високорівневі системи акторів наглядаються з використанням стратегії, що буде рестартувати безскінчено для всіх типів Exception, виключаючи  ActorInitializationException та ActorKilledException, що будуть завершати дитя. Всі інши підійняті виключення будуть ескалуватись, що завершить всю систему акторів.

/: Кореневий вартівник

Кореневий вартівник є прадідом всіх так званих "високорівневих" акторів, та наглядає за всіма особливими акторами, переліченими в Високорівневі сфери та шляхи акторів з використанням SupervisorStrategy.stoppingStrategy, чиє призначення завершити дітей за виникнення любого типу Exception. Всі інші будуть ескаловані.. але куди? Оскільки любий справжній актор має наглядача, наглядач кореневого актора не може бути дійсно актором. Та оскільки це означає, що він “за межами бульбашки”, він називається “той, хто мандрує по бульбашках простору та часу”. Це синтетичний ActorRef, що ефективно завершує свого дитя при перших натяках на проблеми, та встановлює статус для системи акторів isTerminated в true, як тільки кореневий вартівник повністю завершиться (всі діти рекурсивно завершаться).

Що означає рестарт

Коли ми зустрічаємось з актором, що схибив під час обробки певного повідомлення, причини збою підпадають в три категорії:

  • Систематичні помилки (програмування) для окремого отриманного повідомлення
  • (Перехідні) відмови деякого зовнішнього ресурсу, що використовується в процесі обробки повідомлення
  • Зруйнований внутрішній стан актора

Якщо збій не був окремо розпізнаний, не можна виключати третю причину, що призводить до висновку, що внутрішній стан треба очистити. Якщо супервізор вирішає, що його інше дитя або він сам не причасний до пошкодження  — наприклад, через свідоме використання помилки шаблону ядра помилки  — та, таким чином, краще рестартувати дитя. Це зводиться до створення нового примірнику підлеглого класу Actor та заміщення збійного примірнику на свіжий в дитячому ActorRef; можливість зробити це є однією з причин енкапсуляції акторів в спеціальних посиланнях. Потім новий актор відновлює обробляти свою поштову скриньку, що означає, що рестарт не видимий за лаштунками самого актора, з помітним виключенням, що повідомлення, під час якого трапився збій, не буде оброблене повторно. 

Точна послідовність подій на протязі рестарту наступна:

  1. призупинити актора (що означає, що він не буде звичайно обробляти повідомлення до відновлення), та рекурсивно призупинити всіх його дітей
  2. викликати перехоплювач preRestart старого примірнику (по замовчанню надсилає запити на завершення до всіх дітей та викликає postStop)
  3. зачекати (використовуючиcontext.stop()), доки дійсно завершаться всі діти, для яких був запит на завершення під час preRestart; це — як всі операції з акторами  — не блокує, повідомлення завершення авд останнього вбитого дитя буде ефективно просувати до наступного кроку
  4. створити примірник нового актора, викликавши оригінально запроваджену фабрику ще раз
  5. викликати postRestart на новому примірникі (що по замовчанню також викликає preStart)
  6. надіслати запит на рестарт до всіх дітей, що не вбиті під час кроку 3; рестартовані діти будуть слідувати тій же процедурі ресурсивно, від кроку 2
  7. відновити актора

Що означає моніторинг життєвого циклу

Зауваження

Моніторинг життєвого циклу в Akka звичайно відомий як DeathWatch

До контрасту зі спеціальними відносинами між батьками та дітьми, описаними вище, кожний актор може моніторити кожного іншого актора. Оскільки актори з'являються від створення та рестарти не видимі за межами задіяних супервізорів, єдиною зміною стану для моніторингу є перехід від живого до мертвого. Таким чином моніторинг використовується для пов'язання одного актора до іншого, так що він може відреагувати на його завершення, на відміну від супервізора, що реагує на збої.

Моніторинг життєвого циклу реалізоано з використанням повідомлення Terminated, до буде отримано актором що моніторить, та поведінка за замовченням є викликати спеціальне виключення DeathPactException, якщо немає іншої обробки. Щоб почати прослуховувати повідомлення Terminated, викличте  ActorContext.watch(targetActorRef). Щоб перестати слухати, викличтеActorContext.unwatch(targetActorRef). Одна важлива властивість полягає в тому, що ці повідомлення будуть доставлені безвідносно до порядку, в якому відбувались запити на моніторинг та завершення цілей, тобто ви все ще отримаєте повідомлення, навіть якщо на час реєстрації ціль вже була мертвою.

Моніторинг, зокрема, корисний, якщо супервізор не може просто рестартувати своїх дітей, та має завершити їх, тобто в випадку помилок під час ініціалізації актора. В цьому випадку він повинен моніторити ціх дітей, та перестворити їх, або запланувати собі повторити це на пізніший час. 

Інше загальне застосування - це коли актор потребує зхибити за  відсутності зовнішнього ресурсу, що може також бути одним з його дітей. Якщо третя сторона завершує дитя за допомогою методу  system.stop(child), або надсилаючи PoisonPill, це також може вплинути на супервізор.

Відкладені рестрти з шаблоном BackoffSupervisor

Провадячи вбудований шаблон akka.pattern.BackoffSupervisor, актор реалізує так звану стратегію експотенційної затримки нагляду, що може використоуватись для пожиттєвого нагляду актора, та коли він завершиться, спробувати стартувати його знову, кожного разу з більшим часом затримки між ціма рестартами.

Цей шаблон корисний, коли запущений актор схиблює через те, що деякий зовнішній ресурс не доступний, та нам треба отримати деякий час для того, щоб стартувати знову. Один з первинних прикладів, коли це корисне, це коли схиблює PersistentActor зі збоєм постійності  - що вказує, що база даних може бути вимкнена або перевантажена. В такій ситуації має більше сенсу надати трохи більше часу для відновлення, перед тим як постійний актор буде рестартований. 

Наступний клаптик Scala показує, як створювати супервізор з затримкою, що буде стартувати даний echo-актор з наростаючими інтервалами в 3, 6, 12, 24, та, нарешті, 30 секунд:

  1. val childProps = Props(classOf[EchoActor])
  2.  
  3. val supervisor = BackoffSupervisor.props(
  4. childProps,
  5. childName = "myEcho",
  6. minBackoff = 3.seconds,
  7. maxBackoff = 30.seconds,
  8. randomFactor = 0.2) // додає 20% "шуму" щоб трохи варіювати інтервали
  9.  
  10. system.actorOf(supervisor, name = "echoSupervisor")

Це еквівалентно такому коду Java:

  1. import scala.concurrent.duration.Duration;
  1. final Props childProps = Props.create(EchoActor.class);
  2.  
  3. final Props supervisorProps = BackoffSupervisor.props(
  4. childProps,
  5. "myEcho",
  6. Duration.create(3, TimeUnit.SECONDS),
  7. Duration.create(30, TimeUnit.SECONDS),
  8. 0.2);
  9.  
  10. system.actorOf(supervisorProps, "echoSupervisor");

Тут randomFactor використовується для довання трохи більшої варіації до інтервалів затримки, що дуже рекомендоване, щоб уникнути ситуації, коли дкілька акторів рестартують точно в тій же точці часу, наприклад, оскільки вони зупинились через розділений ресурс, такий, як база даних, що впала, та рестарт через той же сконфігурований інтервал. Додавання випадковості до інтервалів рестарту акторів призведе то того, що вони будуть стартувати в трохи інший час, таким чином запобігаючи великих піків трафіку, націленого на розподілену базу даних, або на інший ресурс, з яким їм треба з'єднатись. 

Стратегія кожний-за-себе vs. всі-за-одного

Є дві класичні класи стратегій супервізора, що ідуть з Akka:  OneForOneStrategy та AllForOneStrategy. Обоє сконфігуровані з відображенням з типу виконання на директиви нагляду (дивіться вище above), та обмежують, як часто дитині дозволяється схибити, перш ніж вона буде завершена. Різниця між ними така, що перша застосовує отриману директиву тільки до дитини, що схибила, тоді як друга застосовує також до всіх сестер. Зазавичай ви повинні використовувати OneForOneStrategy, що також є по замовчанню, якщо явно не вказане інше.

Стратегія AllForOneStrategy стосовна в випадках, коли гурт дітей мають такий тісний зв'язок між собою, що збій одного впливає на функцюювання інших, тобто вони нерозривно пов'язані. Оскільки рестарт не очищує поштову скриньку, часто краще завершити дітей під час збою, та явно перестворити їх  з боку супервізора (наглядаючи за життєвим циклом дітей); інакше ви маєте бути впевненим, що для кожного з акторів не виникає проблем, коли він отримав повідомлення, що було поставлене в чергу перед рестартом, але оброблене після.

Звичайно, зупинка дитини (тобто не відповідь на збій) не буде автоматично завершувати інших дітей в стратегії всі-за-одного; це може бути просто зроблене, наглядаючи за їх життєвим циклом: якщо повідомлення Terminated не оброблене супервізором, він підіймає DeathPactException, що (в залежності від свого супервізора) буде рестартувати їх, та дія по замовчанню preRestart буде завершувати дітей. Звичайно, це може бути оброблено також явно.

Занотуйте будь ласка, що створення одноразових акторів в супервізорі всі-за-одного тягне за собою те, що збої, ескаловані тимчасовим актором, будуть впливати на всі постійні актори. Якщо це не бажано, встановіть проміжний супервізор, ; це може бути дуже просто зроблене, декларуючи маршрутизатор розміру 1 для робітника, дивіться: Маршрутизація або тут: Маршрутизація.

Посилання на акторів, шляхи та адреси

Ця глава описує, як актори ідентифікуються, та розміщуються в, можливо розподіленій, системі акторів. Це пов'язане з центральною ідеєєю, що Системи акторів формують внутрішні ієрархії нагляду, так само, як те, що комунікації між акторами прозорі з точки зору їх розташування між декількома вузлами мережі. 

../_images/ActorPath.png

Малюнок вище показує відносини між найбільш важливими сутностями в системі акторів. Деталі читайте далі. 

Що таке посилання на актора?

Посилання на актора є підтипом ActorRef, чиє головне призначення підтримувати надсилання повідомлень до актора, якого воно представляє. Кожний актор має доступ до свого канонічного (локального) посилання через поле self; це посилання також включене як посилання на відправника для всіх повідомлень, надісланих іншим акторам. Навпаки,  впродовж обробки повідомлення актор має доступ до посилання, що представляє відправника поточного повідомлення, через метод sender.

Є декілька різних типів посилань, що підтримуються, в залежності від конфігурації системи акторів:

  • Чисто локальні посилання акторів використовуються системами акторів, які не сконфігуровані для підтримки мережевих функцій. Ці посилання акторів не будуть функціонувати, якщо будуть відіслані через мережеве з'єднання до віддаленої JVM.
  • Локальні посилання акторів, коли дозволені віддалені, використовуються системами акторів, що підтримують мережеві функції для ціх посилань, але ці посилання представляють акторів на тій же JVM. Щоб бути доступними при надсиланні до інших вузлів мережі, ці посилання включають протокол та інформацію про віддалене адресування.
  • Є підтип локальних посилань акторів, що використовуються для маршрутизаторів  (тобто актор з підмішаним трейтом Router). Їх логічна структура такаж сама, як для вищезгаданих локальних посилань, але надсилання повідомлення до них доставляється напряму одному з їх дітей. 
  • Віддалені посилання представляють акторів, що доступні з використанням віддалених комунікацій, тобто надсилання повідомлень до них буде прозоро серіалізувати повідомлення, та надсилати їх на віддалену JVM.
  • Є декілька спеціальних типів посилань акторів, що поводяться як локальні для всіх практичних цілей:
    • PromiseActorRef є спеціальним представленням Promise для цілей бути завершеним через відповідь від актора. akka.pattern.ask створює ці посилання акторів.
    • DeadLetterActorRef є реалізацією по замовчанню для сервісу мертвих повідомлень, до якого Akka направляє всі повідомлення, ції призначення завершились або не існували.
    • EmptyLocalActorRef це те, що повертає Akka, коли бачить неіснуючий шлях локального актора: це еквівалентно до DeadLetterActorRef, але він зберігає свій шлях, так що Akka може надіслати його по мережі та порівняти його з іншими існуючими посиланнями акторів для цього шляха, деякі з яких можуть бути отримані до того, як актор помер.
  • та ще є декілька одноразових внутрішніх реалізацій, які ви насправді ніколи не побачите:
    • Є посилання актора, що не представляє актора, але діє тільки як псевдо-супервізор для кореневого вартівника, ми називаємого його “той, хто мандрує по бульбашках простору та часу”.
    • Перший сервіс журналювання, запущений перед справжнім запуском потужностей, що створюють акторів, є фальшивим актором, що сприймає повідомлення журналювання та друкує їх прямо до стандартного виводу; це Logging.StandardOutLogger.

Що таке шлях актора?

Оскільки актори створюються в суворо ієрархічний спосіб, існує унікальна послідовність імен акторів, шр рекурсивно слідує зв'язкам супервізорів між дітьми та батьками, все далі до кореня системи акторів. Цю послідовність можна розглядатись як вкладені теки файлової системи, тому ми застосовуємо ім'я “шлях” для посилання на них. Як і в деяких реальних файлових системах, є також “символічні зв'язки”, тобто один актор може бути досяжний з використанням більше ніж одного шляху, але всі крім одного включають деяку трансляцію, що відокремлює частину шляху від дійсної лінії супервізорів-предків актора; ці подробиці описані в підрозділах нижче. 

Шлях актора складається з якоря, що ідентифікує систему акторів, за яким слідує поєднання елементів шляху, від кореневого вартівника до вказаного актора; елементи  шляху є іменами перейдених акторів, та розділені косими рисками. 

Яка відмінність між посиланням на актора та його шляхом?

Посилання на актора визначає одного актора, та життєвий цикл посилання співпадає з життєвим циклом актора; шлях актора представляє ім'я, що може відповідати або не відповідати актору, та самий шлях не має життєвого циклу, він ніколи не стає недійсним. Ви можете створити шлях актора без створення актора, але ви не можете створити посилання актора без створення відповідного актора. 

Ви можете створити актора, завершити його, та потім створити нового актора з тим же шляхом. Новий створений актор є новою інкарнацією актора. Це не той самий актор. Посилання на стару інкарнацію актору не буде дійсною для нової інкарнації. Повідомлення, надіслані старому посиланню актора не будуть доставлені новій інкарнації, навіть зважаючи, що вони мають один шлях.

Якори шляха актора

Кожний шлях актора має компонент адреси, що описує протокол та розміщення, по якому можна досягти відповідного актора, за яким слідують імена акторів в ієрархії від кореня вгору. Ось приклади:

  1. "akka://my-sys/user/service-a/worker1" // повністю локальний
  2. "akka.tcp://my-sys@host.example.com:5678/user/service-b" // віддалений

Тут akka.tcp є віддалений транспорт по замовчанню для реліза 2.2; інші транспорт під'єднуються. Віддалений вузол, що використовує UDP, має бути доступний з використанням akka.udp. Інтерпретація вузла та порта (як host.example.com:5678 в прикладі) залежить від використовуваного транспортного механізму, але він має дотримуватись структурним правилам URI.

Логічні шляхи акторів

Унікальний шлях, що отриманий від слідування від батьківського супервізора в напрямку кореневого вартівника називається логічним шляхом актору. Цей шлях точно співпадає з порядком створення акторів, так що від повністью детермінистичний, як тільки встановлена віддалена конфігурація системи акторів (да за допомогоюї її адресний компонент шляху).

Фізичні шляхи акторів

В той час, як логічні шлихи акторів описують функціональне розташування в одній системі акторів, базоване на конфігуації віддалене розвертання означає, що актор може бути створений на вузлах мережі, що не співпадають з вузлом батьківського актора, тобто в іншій системі акторів. В цьому випадку слідування шляхом актора від кореневого вартівника нагору завершиться мандрівкою по мережі, що є коштовною операцією. Таким чином, кожний актор також має фізичний шлях, починаючи з кореневого вартівника системи акторів, де дійсно знаходиться об'єкт актора. Використовуючи цей шлях як посилання на відправника, при запиті інших акторів, дозволить їм відповідати прямо актору, мінімізуючи затримки, викликані машрутизацією. 

Один важливий аспект полягає в тому, що цей фізичний шлях актора ніколи не перетинає декілька систем акторів або JVM. Це означає, що логічний шлях (ієрархія супервізорів) та логічний шлях (розвертання актора) для актора може відрізнятись, якщо один з предків наглядається віддалено.

Як отримати посилання на акторів?

Є дві головні категорії, до яких відносяться методи отримання посилань на акторів: створенням акторів, або шукаючи їх, де перша функціональність іде в двох варіантах: створення посилань актора з конкретного шляху актора, за запит логічної ієрархії актора. 

Створення акторів

Система акторів типово починається зі створення акторів від актора-вартівника, з використанням метода ActorSystem.actorOf, та потім використовувати ActorContext.actorOf від створеним акторів для відтворення дерева акторів. Ці методи повертають посилання на тольки но створеного актора. Кожний актор має прямий доступ  (через ActorContext) для посилання на його батька, самого його, та його дітей. Ці посилання можуть бути надіслані через повідомлення іншим акторам, дозволяючи їм відповідати напряму. 

Пошук акторів по конкретному шляху

На додаток посилання акторів можуть бути знайдені з використанням метода ActorSystem.actorSelection. Може бути використана селекція для комунікації з вказаним актором, та актор, відповідаючий до селекції переглядається, коли доставляється кожне повідомлення.

Для отримання ActorRef, що прив'язаний до життєвого циклу окремого актору, вам треба надіслати повідомлення, таке, як вбудоване повідомлення Identify, до актора, на використовувати посилання  sender() відповіді від актора. 

Абсолютні  vs. відносні шляхи

На додаток до ActorSystem.actorSelection є також  ActorContext.actorSelection, що доступний в кожному актору як  context.actorSelection. Це дає селекцію актора здебільше подібно до його двійника на ActorSystem, але замість пошуку по шляху догори, від кореня дерева актора,  він починає з поточного актора. Елементи шляху, що складаються з двох крапок ("..") можуть використовуваться для доступу до батьківського актора. Ви можете, наприклад, надіслати повідомлення окремому рідному брату (або сестрі):

  1. context.actorSelection("../brother") ! msg

Абсолютний шлях, може, звичайно, також бути знайдений в контексті звичайного шляху, тобто наступне теж буде робити як очікується:

  1. context.actorSelection("/user/serviceA") ! msg

Запит логічної ієрархії актора

Оскільки система акторів формує ієрархію, подібну до файлової системи, порівняння шляхів можливе в той же спосіб, як підтримується в оболонці Unix: ви можете замінити (частину) елементів імені шляху на загальні символи («*» та «?») для формулювання селектора, що може співпадати з нелем або більше акторів. Оскільки результат не є єдиним посиланням актора, він має інший тип ActorSelection, та не підтримує весь набір операцій, що й ActorRef. Селекції можуть бути сформульовані з використанням методів  ActorSystem.actorSelection  та  ActorContext.actorSelection, та підтримують надсилання повідомлень:

  1. context.actorSelection("../*") ! msg

буде надсилати msg всіб братам всім сестрам, включаючи поточного актора. Як і для посилань, отриманних за допомогою actorSelection, виконується перебіг ієрархії, щоб виконати надсилання повідомлення. Позаяк точний набір акторів, що співпадають з селектором, може змінюваться навіть під час того, як повідомлення мандрує до отримувачів, не видається можливим наглядати за селекцією щодо змін життездатності. Щоб зробити це, розрішіть непевність, надсилаючи запити та збираючи всі відповіді, виділяючи посилання на відправника, та потім наглядаючи за всіма виявленими конкретними акторами. Ця схема розрішення селекції може бути покращена в майбутньому релізі.

Підсумок: actorOf vs. actorSelection

Зауваження

Дія перелічених описаних селекторів можна підсумувати та просто запам'ятати наступним чином:

  • actorOf тільки створює нового актора, та він створює його як прямого потомка контексту, в якому викликається цей метод (що може бути любим актором або системою акторів).
  • actorSelection тільки шукає існуючих акторів, куди доставляються повідомлення, тобто не створює акторів, або перевіряє існування акторів при створенні селекції.

Рівність посилань та шляхів акторів

Рівність при порівнянні ActorRef має на увазі, що ActorRef відповідає цільовій інкарнації актора. Два посилання актора рівні при порівнянні, коли вони мають той же шлях та вказують на ту ж саму інкарнацію актора. Посилання, що вказує на завершеного актора, не дає рівності при порівнянні з посиланням, що вказує на іншого (перествореного) актора з тим же шляхом. Зауважте, що рестпрт актора, викликаний збоєм, все ще означає, що це та ж інкарнація актора, тобто рестарт не є видімим для споживача ActorRef.

Якщо вам треба відсліджувати посилання на актора в колекції, та не важлива точна інформація актора, ви можете використовувати  ActorPath як ключ, оскільки ідентифікатор цільового актора не приймається до уваги при порівнянні шляхів акторів.

Повторне використання шляхів акторів

Коли актор завершений, його посилання буде вказувати на мартву поштову скриньку, DeathWatch буде публікувати його фінальне перетворення, та загалом не очікується, що він знову поверненться до життя (оскільки життєвий цикл актора не дозволяє це). Доки можливе створити актора в пізднішій час з ідентичним шляхом — просто через те, що неможливо змусити зворотнє без підтримки набору всіх будь коли створених акторів — це не гарна практика: повідомлення, надіслані з actorSelection до актора, який “died” раптово починає робити знову, але без жодних гарантій порядку між цією переміною та жодною іншою подією, оскільки новий мешканець шляху може отримувати повідомлення, що були призначені до попереднього орендара.

Може бути вірною річчю робити це в дуже особливих випадках, але переконайтесь, що обмежуєте таку обробку до супервізора, оскільки це єдиний актор, що може надійно визначити належну дерегістрацію імені, перед якою створення нової дитини буде схиблювати.

Це також може знадобитись під час тестування, коли ціль тесту залежить від спроможності створити примірник по заданому шляху. В цьому випадку краще обманути його супервізор, так що він буде пересилати повідомлення Terminated в потрібну точку тестової процедури, дозволяючи останньому очікувати належну дерегістрацію імені. 

Взаємодія з віддаленим розгортанням

Коли актор створює дитя, той, хто розгортає систему акторів, буде приймати рішення, чи новий актор залишається на тій же JVM, або на іншому вузлі. В другому випадку створення актора буде перемикатись чере мережеве з'єднання, щоб воно відбувалось в іншій JVM, та, відповідно, в іншій системі акторів. Віддалена система буде роміщувати нового актора нижче спеціального шлязу, зарезервованого для цього призначення, та супервізор нового актора буде посилання віддаленого актора (представляючи того актора, що викликав його створення). В цьому випадку context.parent (посилання супервізора)  та context.path.parent (батьківський вузол в шляху актора) не представляють того ж актора. Однак пошук імені дитини в супервізорі буде знаходити його на віддаленому вузлі, зберігаючи логічну структуру, тобто потім надсилаючи на нерозрішене посилання актора.

../_images/RemoteDeployment.png

Для чого використовується адресна частина?

Коли посилання актора надсилається по мережі, воно представлене як шлях. Таким чином, шлях мусить повністю кодувати всю інформацію, потрібну для надсилання повідомлень потрібному актору. Це досягаєтсья завдяки кодуванню протокола, вузла та порта в адресній частині рядка шляху. Коли система акторів отримує шлях актора від віддаленого вузла, вона перевіряє, чи адреса шляху співпадає з адресою системи акторів, в якому випадку він буде розрішений до локального посилання актора. Інакше він буде представлений віддаленим посиланням актора. 

Сфери високого рівня для шляхів актора

В корені шляху ієрархії шляху розташований кореневий вартівник, над яким знаходяться всі інші актори; його ім'я "/". Наступний рівень складається з наступного:

  • "/user" актор-вартівник для всіх акторів, створених користувачем на верхньому рівні; актори, створені з використанням  ActorSystem.actorOf знаходяться під цім шляхом.
  • "/system" є актор-вартівник для всіх акторів, створених системою на вищому рівні, тобто слухачів журналювання, або акторів, що автоматично розгортаються конфігурацією при запуску системи акторів. 
  • "/deadLetters" є актор мертвих листів, що є місцем, куди завертають всі повідомлення, надіслані до неіснуючих акторів (на основі кращої спроби: повідомлення можуть загубитись навіть на локальній JVM).
  • "/temp" є вартівником для швидко-живучих створених системою акторів, тобто тих, що використовуються в реалізації ActorRef.ask.
  • "/remote" є штучним шляхом, під яким знаходяться всі актори, чиї супервізори є посиланнями на віддалених акторів

Потреба структурувати простір імен для акторів, як це, постає з центральної та дуже важливої цілі: все в ієрархії є актором, та всі актори функціонують в той же спосіб. Оскільки ви можете не тільки шукати акторів, яки ви створюєте, в можете також шукати системного вартівника, та надсилать йому повідомлення (яке він буде покірно  відкидати в цьому випадку). Цей потужний принцип означає, що немає особливостей, які треба пам'ятати, що робить цілу систему більш однорідною та послідовною.

Якщо ви бажаєте прочитати більше щодо високорівневої структури системи акторів, погляньте на Супервізори вищого рівня.

Прозорість розташування

Попередній розділ описує, як використовуються шляхи акторів, що дозволяє прозорість розташування. Це спеціальна можливість потребує дещо більшого пояснення, оскільки пов'язаний термін “прозоре віддалення” використовувалось досить різноманітно в контексті мов програмування, платформ та технологій.

Розподілений по замовчанню

Все в Akka розроблене для роботи в розподіленій установці: всі інтеракції акторів використовують чисту передачу повідомлень, та все є асинхронним. Цей підхід був обраний, щоб впевнитись, що всі функції доступні однаково при роботі на одній JVM, або на кластері з сотен машин. Ключ для впровадження цього це перехід від віддаленого до локального шляхом оптимізації, заміть того, щоб намагатись пройти від локального до віддаленого, шлягом узагальнення. Дивіться цей класичний папір для детальної дискуссії, чому другий підхід приречений на невдачу.

Шляхи, якими руйнується прозорість

Що є вірним для Akka, не обов'язково вірне для застосування, що використовує його, оскільки розробка для розподіленого виконання накладає деякі обмеження того, що можливе. Найбільш очевидним є те, що всі повідомлення, надіслані по дроті, повинні бути серіалізуємі. Хоч трохи менш очевидно, це включає замикання, що використовуються як фабрики акторів (тобто в Props), якщо актор буде створюватись на віддаленому вузлі.

Іншим наслідком є те, що все має бути готове до того, що всі взаємодії будуть повністю асинхронним,  що в комп'ютерній мережі може означати, що похід повідомлення до отримувача може зайняти декілька хвилин (в залежності від конфігурації). Це також означає, що вірогідність втрати повідомлення значно вища, ніж на одній JVM, де воно близьке до нуля (але все ще без твордих гарантій!).

Як робить віддалення?

Ми довели ідею прозорості до межі в тому сенсі, що майже немає API для прошарку віддалення в Akka: це повністю залежить від конфігурації. Просто напишіть ваше застосування відносно до принципів, змальованих в попередніх розділах, потім задайте віддалене розгортання дерев акторів в файлі конфігурації. Таким чином ваше застосування може бути маштабоване без потреби торкатися коду. Єдина частина API, яка має програмний вплив на віддалене розгортання, це те, що Props містить поле, що може бути встановлене в певний примірник Deploy; це матиме той же ефект, що і покласти еквівалентне розгортання в файл конфігурації (якщо задані обоє, файл конфігурації перемагає).

Точка-точка vs. клієнт-сервер

Akka Remoting є модулем комунікації для поєднання систем акторів в стилі точка-точка, та є основою для Akka Clustering. Розробка віддалення рухається двома (пов'язаними) рішеннями:

  1. Комунікація між системами симетрична: якщо система A може з'єднатись з системою B, тлді система B мусить також бути в змозі з'єднатись з системою A незалежним чином.
  2. Роль комунікаційних систем симетрична в сенсі шаблонів з'єднання: немає системи, що тільки сприймає з'єднання, та немає системи, що тільки ініціює з'єднання.

Слідоцтвом з ціх рішень є те, що неможливо безпечно створити чисту клієнт-серверну установку зі здалегідь визначеними ролями (порушення допущення 2). Для клієнт-серверних установок краще використовувати HTTP або Akka I/O.

Важливо: Використання установок, що включають Network Address Translation, Load Balancers або контейнери Docker порушують припущення 1, якщо не були задіяні додаткові кроки конфігурації мережі, що дозволяють симетричні комунікації між причетними системами. В таких ситуаціях Akka може бути сконфігуровано для прив'язки до іншої мережевої адреси, інж та, що використовується для встановлення з'вязку між вузлами  Akka. Дивіться Віддалена конфігурація для NAT та Docker.

Точки помітки для маштабування вгору за допомогою маршрутизаторів

На додаток до можливості виконувати різні частини системи акторівна різних вузлах кластеру, також можливе маштабуватись вгору на більше число ядер, примножуючи суб-дерева акторів, що підтримує паралелізацію (думайте, наприклад, про пошукоу машину, що підтримує різні запити пошуку одночасно). Клони потім можуть бути маршрутизовані різним чином, наприклад, по кругу. Єдиною річчю, необхідною щоб досягти це, це розробник має декларувати певного актора як “withRouter”, потім  — на його місці — буде створений актор-маршрутизатор, що буде відроджувати конфігуровану кількість дітей бажаного типу, та маршрутизувати до них сконфігурованим чином. Тільки такий маршрутизатор буде декларовано, його конфігурація може бути вільно перекрати з файлу конфігурації, включаючи змішування з віддаленим розгортанням (деяких) дітей. Читайте більше щодо цього в Маршрутизація (Scala) та Маршрутизація (Java).

Akka та модель пам'яті Java

Головна вигода від використання Typesafe Platform, включаючи Scala та Akka, в тому, що це спощує процес написання конкурентних програм. Ця стаття дискусує, як Typesafe Platform, та зокрема Akka, підходить до розподіленої пам'яті в конкурентних застосуваннях.

Модель пам'яті Java

До Java 5 модель пам'яті Java Memory Model (JMM) була хворобливо визначена. Було можливим отримати всі типи дивних результатів, коли розділена пам'ять використовувалась з декількох потоків, як:

  • потік не бачив значення, записані іншими потоками: проблема видимості
  • потік бічив 'неможливу' поведінку інших потоків, що викликалось виконанням інструкцій в порядку, що не був очікуваний: проблема перестановки інструкцій.

З реалізацією JSR 133 в Java 5 багато з ціх питань були вирішені. JMM є набором правил, що базуються на відношенні "відбуватись-раніше", що обмежує, коли один доступ до пам'яті відбувається перед іншим, та навпаки, коли їм дозволяється траплятись не у порядку. Два приклада ціх правил:

  • Правило моніторингу блокування: вивільнення блокування відбувається перед кожним наступним захопленням того ж блокування. 
  • Правило непостійної змінної: запис непостійної змінної відбувається перед наступним читанням тієї самої непостійної змінної

Хоча JMM може виглядати заскладним, специфікація намагається знайти баланс між простотою використання, та можливістю писати продуктівні та маштабуємі конкурентні структури даних.

Актори та Java Memory Model

З реалізацією акторів в Akka, є два шляхи, якими потокі можуть виконувати акторів в розподіленій пам'яті:

  • якщо повідомлення надсилається до актора (іншим актором). В більшості випадків повідомлення незмінні, але якщо це повідомлення не є вірно сконструйованим незмінним об'єктом, без правила "відбувається раніше" , може бути можливим для отримувача бачити частково ініціалізовані структури даних, та, можливо, навіть значення зі стелі повітря  (long/double).
  • якщо актор робить зміни до свого внутрішнього стану під час обробки повідомлення, то отримує доступ до того стану під час обробки іншого повідомлення декілька моментів пізніше. Важливо уявляти, що з моделлю акторів ви не отримуєте жодних гарантій, що той же потік буде виконувати того ж актора для різних повідомлень.

Щоб уникнути проблем видимості та перестановки для акторів, Akka гарантує наступні два правила "відбуватись раніше":

  • Правило надсилання актора: надсилання повідомлення до актора відбувається перед отриманням цього повідомлення тим же актором. 
  • Правило послідовної обробки актора: обробка одного повідомлення відбувається перед обробкою наступного повідомлення тим же актором. 

Зауваження

В непрофесійних термінах це означає, що зміни до внутрішніх полів актора видимі, коли наступне повідомлення обробляється цім актором. Так що полі вашого актора не мають бути непостійними або еквівалентними.

Обоє правил стосуються тільки того ж примірника актора, та не діють, якщо використовуються різні актори.

Future та Java Memory Model

Завершення Future "відбувається перед" викликом любого зворотнього виклику, зареєстрованого для виконання.

Ми рекомендуємо не замикати на не-фінальні поля (final в Java та val в Scala), та якщо ви замикаєте на нефінальні поля, вони мають бути зроблені volatile, щоб поточне значення поля було дидиме в зворотньому виклику. 

Якщо ви замикаєте на посиланні, ви повинні також переконатись, що примірник, на який посилаються, безпечний щодо потоків. Ми дуже рекомендуємо стояти осторонь об'єктів, що використовують блокування, оскільки це може ввести проблеми продуктивності, та, в гіршому випадку, до тупих кутів. Це є небезпеками синхронізації.

STM та  Java Memory Model

Akka's Software Transactional Memory (STM) також провадить правило "відбувається перед":

  • Правило транзакційного посилання: вдалий запис під час закріплення, на посиланні до транзакції, відбувається перед любим наступним читанням того ж транзакційного посилання.

Це правило виглядає більше як правило 'непостійної змінної' з JMM. Наразі Akka STM підтримує тільки відкладений запис, так що дійсний запис до розподіленої пам'яті відкладений до закріплення транзакції. Запис впродовж транзакції покладається в локальний буфер (набір записів транзакції), та не видимий іншим транзакціям. Ось чому бруднічитання неможливі.

Як ці правила реалізовані в Akka є деталями реалізації, та можуть змінюватись з часом, та конкретні деталі можуть навіть залежати від використаної конфігурації. Але вони будуть будуватись на інших правилах  JMM, як правило моніторингу блокування, або правило непостійної змінної. Це означає що ви, користувач Akka, не маєте хвилюватись щодо додавання синхронізації, що впровадити такі відносини "відбувається раніше", оскільки це обов'язок Akka. Так що ви маєте руки вільними, щоб вирішувати вашу базову логіку, та фреймворк Akka запевняє, що ці правила гарантовано прислужаться вам. 

Актори та розподілений змінний стан

Оскільки Akka виконується в JVM, є ще деякі правила, що їм треба слідувати.

  • Замикання на внутрішньому стані актора та демонстрація цього іншим потокам
  1. class MyActor extends Actor {
  2. var state = ...
  3. def receive = {
  4. case _ =>
  5. // Невірно
  6.  
  7. // Дуже погано, розподілений змінний стан
  8. // буде руйнувати ваше застосування збоченим шляхом
  9. Future { state = NewState }
  10. anotherActor ? message onSuccess { r => state = r }
  11.  
  12. // Дуже погано, "sender" змінюється в кожному повідомленні,
  13. // баґ розподіленого змінного стану
  14. Future { expensiveCalculation(sender()) }
  15.  
  16. // Правильнощі
  17.  
  18. // Повністю безпечно, на "self" можна замикати
  19. // та це ActorRef, що безпечний до потоків
  20. Future { expensiveCalculation() } onComplete { f => self ! f.value.get }
  21.  
  22. // Повністю безпечно, ми замикаємо на фіксованому значенні
  23. // та це ActorRef, що безпечний до потоків
  24. val currentSender = sender()
  25. Future { expensiveCalculation(currentSender) }
  26. }
  27. }
  • Повідомлення повинні бути незмінними, щоб уникнути пастки розподіленого змінного стану.

Надійність доставки повідомлень

Akka допомагає вам будувати надійні застосування, що використовують декілька процесорних ядер машини (“маштабування догори”) або розподілятись по комп'ютерній мережі (“маштабування вшир”). Ключовою абстракцією, що змушує це робити, це те, що всі взаємодії між частинами вашого коду — акторами — відбувається через передачу повідомлень, ось чому чтона семантика того, як повідомлення передаються між акторами заслуговує на свою окрему главу. 

Щоб отримати деякий контекст для дискусії нижче, уявіть застосування, що розташоване на декількох мережевих вузлах. Базовий механізм для комунікації є той же, чи посилається до актора на локальній JVM, або до віддаленого актора, але, звичайно, будуть помітна різниця в латентності доставки (можливо також залежність від полоси пропускання мережевого з'єднання, та розміру повідомлення), та надійність. В випадку, коли надсилається віддалене повідомлення, є очевидним, що задіяно більше кроків, що означає, що більше речей може піти не так. Інший аспект локального надсилання буде в простій передачі посилання на повідомлення всередині тієї ж JVM, без жодний обмежень до підлеглого об'єкту, що наісланий, тоді як віддалений транспорт буде накладати ліміт на розмір повідомлення. 

Написання ваших акторів так, що кожна інтеракція можливо буде віддаленою, є безпечною, песімістичною ставкою. Це означає, що треба покладатись тільки на ті властивості, що завжди гарантовані, та які дискутуються детально нижче. Це, звичайно, має деяке навантаження в реалізації актора. Якщо ви бажаєте поступитись повнії прозорості розміщення — наприклад, в випадку групи тісно співпрацюючих акторів — ви можете розміщувати іх завжди на тій самій JVM, та насолоджуватись жосткішими гарантіями доставки повідомлень. Деталі та компроміси пояснені далі нижче.

В якості додатка ми надаємо декілкьа вказівок, як побудувату міцнішу надійність  на основі вбудованої. Ця глава закривається дискусією про роль “Офісу померлих листів” (Dead Letter Office).

Головні правила

Це правила для надсилання повідомлень (тобто методів tell або !, що також відповідають шаблону ask):

  • доставити-щонайбільше-раз, тобто доставка не гарантована
  • порядок повідомлень для пари відсилач-отримувач

Перше правило типово можна знайти в інших реалізаціях акторів, тоді як друге специфічне для Akka.

Дискусія: що означає “щонайбільше-раз”?

Коли доходить до опису семантики механізму доставки, є три базові категорії:

  • щонайбільше-раз доставка означає, що для кожного повідомлення, переданого до механізму, це повідомлення буде доставлене нуль або один раз; в більш звичайних термінах це означає, що повідомлення може бути втрачене.
  • щонайменьше-раз доставка означає, що для кожного повідомлення, переданого до механізму, потенційно робиться кілька спроб щодо його доставки, так що щонайменьше має бути успішним; знову, в більш звичайних термінах це означає, що може виникнути дублікація, але не втрата.
  • точно-раз доставка означає, що для кожного повідомлення, переданого до механізму, відбувається рівно одна досавка до отримувача; повідомлення не може не бути втрачене, а ні продубліковано.

Перший випадок найдешевший — вища продуктивність, менше навантаження реалізації — оскільки вона може бути виконана в стилі підпалив-і-забув, без зберігання стану на надсилаючій стороні або в механізмі передачі. Друге потребує повторів для підрахунку транспортних страт, що означає зберігання стану на стороні відсилки та мати механізм підтвердження на стороні підсилки. Третє найбільш коштовне — та отже має найгіршу продуктивність — оскільки на додаток до другого він потребує, щоб стан зберігався і на боці отримувача, щоб фільтрувати дублікуючі доставки.

Дискусія: чому немає гарантованої доставки?

В корені проблеми лежить питання, що саме повинне означати гарантовано:

  1. Повідомлення було надіслане з мережі?
  2. Повідомлення передане іншим вузлом?
  3. Повідомлення покладене в поштову скриньку цільового актора?
  4. Повідомлення почало оброблятись цільловим актором?
  5. Повідомлення умпішно оброблене цільовим актором?

Кожне з перерахованого має різні виклики та вартість, та є очевиним, що є умови, коли люба бібліотека передачі повідомленнь не буде в змозі задовільмити; уявіть, наприклад, конфігуровані типи поштові скриньки, та як обмежена поштова скринька буде взаємодіяти з третім пунктом, або навіть що це означає визначитись з частиною “успішно” п'ятого пункта.

За цім слідують мірківання щодо того, що Ніхто не потребує надійної доставки. Єдиним осмисленим шляхом для надсилача знати, чи взаємодія була успішною, є отримання повідомлення підтвердження бізнес рівня, що не є тим, що Akka може зробити сама по собі (ми ані пишемо фреймворк “роби що я маю на увазі”, ані ви не бажаєте, щоб ми це робили).

Akka обіймає розподілене обчислювання, та робить схильність до падінь комунікацій явною через передачу повідомлень, і таким чином не намагається брехати та емулювати діряву абстракцію. Це модель, що з великим успіхом використовувалась в Erlang, та потребує від користувачів розробляти свої застосування коло неї. Ви можете прочитати більше щодо цього підходу в документації Erlang (розділи 10.9 та 10.10), Akka близько слідує йому.

Інший аспект цієї проблеми в тому, що провадження тільки базових гарантій тих випадків використання, що не потребують сильнішої надійності, що не додають вартості до їх реалізації; є завжди можливим додати сильнішу надійність зверху базових, але не можливо заднім числом видалити надійність, щоб отримати більшу продуктивність.

Дискусія: впорядкування повідомлень

Правило, більш конкретно, полягає в тому, що для даної пари акторів, повідомлення, надіслані напряму від першого до другого, не будуть отримані невпорядковані. Слово напряму наголошує, що ця гарантія стосується тільки коли надсилається за допомогою telloperator до кінцевого пункту призначення, але не тоді, коли задіються медіатори, або інші можливості поширення повідомлень (коли не вказане інше).

Гарантія ілюструється наступним чином:

Актор A1 надсилає повідомлення M1, M2, M3 до A2

Актор A3 надсилає повідомлення M4, M5, M6 до A2

Це означає наступне:
  1. Якщо M1 доставлене, воно має бути доставлене до M2 та M3
  2. Якщо M2 доставлене, воно має бути доставлене до M3
  3. Якщо M4 доставлене, воно має бути доставлене до M5 та M6
  4. Якщо M5 доставлене, воно має бути доставлене до M6
  5. A2 може бачити повідомлення від A1 перемішані з повідомленнями від A3
  6. Оскільки немає гарантії доставки, кожне з повідомлень може бути загублене, тобто не надійти до A2

Зауваження

Важливо зауважити, що гарантія Akka стосується до порядку, в якому повідомлення ставляться в чергу до поштової скриньки реціпієнта. Якщо реалізація поштової скриньки не притрумується порядку FIFO (тобто, PriorityMailbox), тоді порядок обробки актором може відхилятись від порядку постановки в чергу.

Будь ласка, зауважте, що це павило не транзитивне:

Актор A надсилає повідомлення M1 актору C

Актор A потім надсилає повідомлення M2 актору B

Актор B пересилає повідомлення M2 актору C

Актор C може отримати M1 та M2 в довільному порядку

Принагідне транзитивне впорядкування буде мати на увазі, щоM2 ніколи не буде отримане перед M1 atна акторіC (хоча кожне з них може бути втрачене). Цей порядок може бути порушений через різні затримки доставлення повідомлень, колиA, B та C знаходяться на різних вузлах мережі, більше дивіться нижче.

Зауваження

Створення актора трактується як повідомлення, надіслане від батька до дитини, з тою ж семантикою, що обгорювалась вище. Надсилання повідомлення до актора в спосіб, що може бути перевпорядкований цім початковим повідомленням створення означає, що повідомлення може не з'явитись, оскільки актор ще не існує. Приклад, коли повідомлення може з'яаитись дуже рано, може бути спроба створити віддалено розташованого актора R1, надсилання його посилання до іншого віддаленого актора R2, та цей R2 надсилає повідомлення до R1. Приклад гарно визначеного впордкування є батько, який створює актора, та безпосередньо надсилає йому повідомлення.

Комунікація відмови

Будь ласка зауважте, що гарантії порядку, дискутовані вище, дотримуються для користувацьких повідомлень між акторами. Відмова дитини актора комунікується через спеціальну систему повідомлень, що не впорядковані відповідно до звичайних користувацьких повідомлень. Зокрема:

Актор-дитина C надсилає повідомлення M батьку P

Актор-дитина дає збій F

Батько P може отримати два повідомлення в любому порядку M, F або F, M

Причина полягає в тому, що внутрішня система повідомлень має свої власні поштові скриньки, так що порядок викликів черги користувацьких та системних повідомлень не може гарантувати порядок часу їх отримання з черги.

Правила для повідомлень локальної JVM

Будьте уважні щодо того, що робити з цім розділом!

Покладання на сильнішу надійність цього розділу не рекомендована, оскільки це може прив'язати ваше застосування до тільки локального розгортання: застосування може бути розроблене інакше (на відміну від застосування тільки деяких шаблонів обміну повідомленнями, локальних для деяких акторів), щоб виконуватись на кластері машин. Наше кредо є “розробляємо раз, розгортаємо де побажаєте”, та щоб досягти цього вам треба покладатись тільки на Загальні правила.

Надійність посилок на локальній машині

Тестова сюїта Akka покладається на те, що в локальному контексті повідомлення не губляться (та для тестів на умову відсутності помилок і для віддаленого розгортання), що значить, що ми насправді докладаємо найкращих зусиль, щоб утримуват наші тести стабільними. Однак локальна операція tell може схибити з деякої причини, так само, як звичайний виклик метода на JVM:

  • StackOverflowError
  • OutOfMemoryError
  • інша VirtualMachineError

На додаток локальні посилки можуть схибити у специфічний для Akka спосіб:

  • якщо поштова скринька не примає повідомлення (тобто повний BoundedMailbox)
  • якщо отримуючий актор схибить при обробці повідомлення, або вже завершений

Коли перше є очевидним наслідком конфігурації, друге є приводом замислитись: надсилач повідомлення не отримує зворотнього зв'язку, якщо під час обробки виникло виключення, замість цього це повідомлення іде до супервізора. Загалом, для зовнішнього спостерігача, це неможливо відрізнити від втрати повідомлення.

Порядок надсилань локальних повідомлень

Якщо припустити поштові скриньки з суворим FIFO, вже зазначене застереження щодо відсутності гарантії транзитивності порядку повідомлень відсутнє за деяких умов. Як ви можете зазначити, це самі по собі дуже тонкі матерії, та навіть можливо, що подальші оптимізації продуктивності зроблять недійсним весь цей параграф. Можливо, неповний перелік контр-показань наступний:

  • Перед отриманням першої відповіді від актора верхнього рівня, існує блокування, що захищає внутрішню проміжну чергу, та цей блок не є чесним; наслідок полягає в тому, що запити в черзі від різних надсилачів, що надходять на протязі конструювання актора (фігурально, деталі більш складні), можуть бути переставлені, в залежності від низькорівневого планування потоків. Оскільки на JVM  не існують повністю чесні блокування, це неможливо полагодити.
  • Той же механізм використовується на протязі конструювання the construction of a Router-а, біль точно маршрутизованого ActorRef, оскільки та ж проблема існує для акторів, розгорнутих за допомогою Routers.
  • Як зазначено вище, проблема виникає скрізь, де задіяне блокування в процесі постаки в чергу, що також може стосуватись власних поштових скриньок.

Цей список був уважно підібраний, але інші проблематичні сценарії, можливо, уникли нашого аналізу.

Як локальне впорядкування співвідноситься з мережевим впорядкуванням

Правило полягає в тому, що, для окремої пари акторів, повідомлення, надіслані безпосередньо від першого до другого, не будуть отримані з порушенням порядку, якщо повідомлення надсилаються через мережу за допомогою базованого на TCP віддаленого транспортного протоколу Akka.

Як пояснено в попередньому розділі, надіслані локальні повідомлення підкорюються випадковому транзитивному порядку за певних умов. Цей порядок може бути порушений через різні затримки доставки повідомлень. Наприклад:

Актор A на node-1 надсилає повідомлення M1 актору C на node-3

Актор A на node-1 потім надсилає повідомлення M2 актору B на node-2

Актор B на node-2 пересилає повідомлення M2 актору C на node-3

Актор C може отримати M1 та M2 в довільному порядку

Для M1 може знадобитись більше часу для "мандрівки" до node-3, ніж забирає "мандрівка" M2 до node-3 через node-2.

Абстракції вищого порядку

Базуючись на малому та послідовному наборі інструментів, Akka також провадить потужні, високорівневі абстракції зверху них.

Шаблони обміну повдіомленнями

Як дискутувалось вище, прямолінійна відповідь на запит надійної доставки є явний протокол ACK–RETRY. В простішій формі це потребує наступне

  • спосіб ідентифікувати окремі повідомлення, щоб пов'язати повідомлення з підтвердженням
  • механіз повторів, що буде знову надсилати повідомлення, якщо підтвердження не прийшло вчасно
  • спосіб для отримувача визначити та відкинути дублікати

Третє стає потрібним завдяки тому, що доставк підтвердження також не гарантована. Протокол ACK-RETRY з підтвердженнями бізнес-рівня підтримується Щонайменьше-раз доставкою модуля Akka Persistence. Дублікати можуть бути виявлені через відсліджування ідентифікаторів повідомлень, відісланих через Щонайменьши-раз дооставку. Інший шлях реалізувати третю частину - це зробити обробку повідомлень незалежною на рівні бізнес логіки.

Інший приклад реалізації всіх трьох вимог відомий як Шаблон надійного проксі (що зараз замінений Щонайменьше-раз доставкою).

Підбір подій

Підбір подій (та шардінг) -  це те, що дозволяє великим веб сайтам зростати до мільярдів користувачів, хоча ідея є досить простою: коли компонент (в нашому випадку актор) обробляє команду, він генерує список подій, що представляють ефект команди. Ці події на додаток застосовуються до стану актора. Гарна річ щодо цієї схеми в тому, що події тільки додаються до сховища, ніщо ніколи на змінюється; це дозволяє чудову реплікацію та маштабування споживачів цього потоку подій (тобто інші компоненти можуть споживати потік подій як спосіб реплікації стану компонента на іншому континенті, або як реакція на зміни). Якщо стан компонента втрачено — через відмову машини або тому що він виштовхнутий з кеша — він може бути легко реконструйований програванням потоку подій (звичайно задіявши знімок, щоб прискорити процес). Підбір подій підтримується Akka Persistence.

Поштова скринька з явним підтвердженням

Реалізуючи власний тип поштової скриньки, можливо повторити обробку повідомлення з боку отримуючого актора, щоб обробити тимчасові збої. Цей шаблон найбільш корисний в локальному контексті комунікації, коли гарантії доставки і так достатні для потреб застосування.

Будь ласка зазначте, що застосовуються особливості для Правил локального надсилання JVM.

Приклад реалізації цього шаблона показаний в Поштовій скриньці з явним підтвердженням.

Мертві листи

Повідомлення, що не можуть бути доставлені (та для яких це можна стверджувати) будуть доставлені до синтетичного актора на ім'я/deadLetters. Ця доставка відбувається на основі кращого-зусилля; при цьому збій може відбутись навіть на локальній JVM (наприклад під час завершення актора). Повідомлення, надіслані через ненадійі мережеві транспорти будуть губитись, не перетворюючись на мертві листи.

Для чого я можу використовувати мертві листи?

Головне використання цієї можливості є налаштування, особливо якщо посилки акторана з'являються узгоджено (тоді, можливо, перегляд мертвих листів підкаже вам, що надсилач або отримувач був десь налаштований неврно). Щоб бути корисними в цей спосіб, є гарною практикою уникати надсилання deadLetters, якщо це можливо, тобто час від часу виконувати застосування з підходящим логером мертвих листів (діивіться нижче), та очищати вивід логера. Ця вправа — я все інше — потребує розважливого застосування здорового грузду: дуже може статись, що запобігання надсилання завершеному актору ускладнює код надсилача, більше, ніж отримується через ясність виводу налаштування.

Сервіс мертвих листів слідує тим же правилам, з повагою до гарантій доставки, що і надсилання інших повідомлень, так що це не може використовуватись для гарантованої доставки.

Як мені отримувати мертві листи?

Актор може підписатись на клас akka.actor.DeadLetter на потоці подій, дивіться Потік подій (Java) або Потік подій (Scala), щоб подивитись як це робиться. Підписаний актор буде отримувати всі мертві листи, опубліковані в (локальній) системі з цього часу і надалі. Мертві листи не передаються по мережі, якщо ви бажаєте збирати їх в одному місці, вам треба підписати по одному актору на кожний вузол, та потім переправляти їх вручну. Також майте не увазі, що мертві листи генеруються на тому вузлі, який може визначити, що операція надсилання схибила, що для віддаленого надсилання може бути локальна система (якщо мережеве з'єднання не встановлене), або віддалений вузол (якщо актор, до якого іде повідомлення, не існує в цей проміжок часу).

Мертві листи, що (звичано) не викликають турботи

Кожного разу, коли актор не завершується за власним рішенням, є шанс, що деякі повідомлення, що він надсилає собі, будуть втрачені. Є дещо, що трапляється досить просто в складних сценаріях завершення, що часто відбуваються: надсилання повідомлення akka.dispatch.Terminate відкидається, що означає, що були надані два запити на завершення, але, звичайно, тільки один був успішним. В тому ж дусі ви можете бачити повідомленняakka.actor.Terminated від дітей при зупинці ієрархії акторів, що перетворюються на мертві листи, якщо батько все ще наглядає за дітьми, коли сам батько завершується.

Конфігурація

Ви можете почати використовувати Akka без визначення жодної конфігурації, оскільки провадяться осмислені значення по замовчанню. Потім вам може стати потрібно змінити налаштування, щоб змінити поведінку по замовчанню, або адаптуватись під специфічні середовища виконання. Типовимі приклади налаштувань, які ви можете змінити:

  • рівень журналу та сам логер
  • ввімкнути віддалене розгортання
  • серіалайзери повідомлень
  • визначення маршрутизаторів
  • підгонка диспечерів

Akka використовує бібліотеку Typesafe Config Library, що також може бути гарним вибором  для конфігурації вашого власного застосування або бібліотеки, створених за допомогою, або без, Akka. Ця бібліотека реалізована в Java без зовнішніх залежностей; вам слідує переглянути її документацію (зокрема щодо ConfigFactory), що тільки підсумовується далі.

Попередження

Якщо ви використовуєте Akka в Scala REPL від серії 2.9.x, та ви не провадите ваш власний ClassLoader для ActorSystem, запустіть REPL  з "-Yrepl-sync", щоб обійти недолік в наданому REPL Context ClassLoader.

Звідки читається конфігурація

Вся конфігурацію для Akka утримується в примірниках ActorSystem, або, кажучи іншими словами, як це бачиться ззовні,ActorSystem є єдиним споживачем інформації конфігурації. Коли створюється система акторів ви можете або передати їй об'єктConfig, або ні, при чому другий випадок еквівалентний передачі ConfigFactory.load() (з потрібним завантажувачем класа). Грубо кажучи це означає, що по замовчанню розбираються всі application.conf, application.json та application.properties, знайдені в корені шляху classpath — будь ласка, звертайтесь до вищезгаданої документації щодо деталей. Система акторів потім поєднує всі ресурси з reference.conf, знайдених в корені classpath, щоб зформувати конфігурацію для відкату, тобто внутрішньо використовує 

  1. appConfig.withFallback(ConfigFactory.defaultReference(classLoader))

Філософія полягає в тому, що код ніколи не містить значення по замовчанню, але замість цього покладається на їх присутність в reference.conf, що надається разом з розгляданою бібліотекою.

Найвищий приоритет надається значенням, що перекривають надані системні властивості, дивіться специфікацію HOCON (ближче до кінця). Також варто зауважити, що конфігурація застосування — що по замовчанню  application  — може бути переписана з використаннямвластивостіconfig.resource (щодо деталей будь ласка посилайтесь до документації з конфігурації).

Зауваження

Якщо ви пишете застосування Akka, тримайте свою конфігурацію в application.conf в корені classpath. Якщо ви пишете бібліотеку на основі Akka, тримайте конфігурацію в reference.conf в корені JAR файла.

Випадок використання JarJar, OneJar, Assembly або іншого jar-бандлера

Попередження

Підхід до конфігурації Akka значно покладається на нотацію, коли модуль/jar має свій власний файл reference.conf, все це може бути виявлене через конфігурацію та завантажено. На жаль це також означає, що якщо ви складаєте/об'єднуєте декілька jar в такий самий jar, вам треба також злити всі reference.confs. Інакше всі замовчання будуть втрачені, та Akka не буде функціонувати.

Якщо ви використовуєте Maven для пакування вашого застосування, ви також можете використовувати підтримку Apache Maven Shade Plugin для Resource Transformers, щоб об'єднувати всі reference.confs по classpath побудови в один.

Конфігурація плагіна може виглядати так:

  1. <plugin>
  2. <groupId>org.apache.maven.pluginsgroupId>
  3. <artifactId>maven-shade-pluginartifactId>
  4. <version>1.5version>
  5. <executions>
  6. <execution>
  7. <phase>packagephase>
  8. <goals>
  9. <goal>shadegoal>
  10. goals>
  11. <configuration>
  12. <shadedArtifactAttached>trueshadedArtifactAttached>
  13. <shadedClassifierName>allinoneshadedClassifierName>
  14. <artifactSet>
  15. <includes>
  16. <include>*:*include>
  17. includes>
  18. artifactSet>
  19. <transformers>
  20. <transformer
  21. implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
  22. <resource>reference.confresource>
  23. transformer>
  24. <transformer
  25. implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
  26. <manifestEntries>
  27. <Main-Class>akka.MainMain-Class>
  28. manifestEntries>
  29. transformer>
  30. transformers>
  31. configuration>
  32. execution>
  33. executions>
  34. plugin>

Власний application.conf

Власний application.conf може виглядати так:

  1. # В цьому файлі ви можете перекрити опції, визначені в файлах посилання.
  2. # Копіюйте частинами з файлів посилань та змінюйте за бажанням.
  3.  
  4. akka {
  5.  
  6. # Логери для реєстрації під час завантаження
  7. # (akka.event.Logging$DefaultLogger пише до STDOUT)
  8. loggers = ["akka.event.slf4j.Slf4jLogger"]
  9.  
  10. # Рівень журналу для використання сконфігурованим логером (див "логери")
  11. # як тільки пони стартують; до того дивіться "stdout-loglevel"
  12. # Опції: OFF, ERROR, WARNING, INFO, DEBUG
  13. loglevel = "DEBUG"
  14.  
  15. # Рівень журналу для дуже базового логера, активованого впрдовж запуску
  16. # ActorSystem. Цей логер друкує повідомлення в stdout (System.out).
  17. # Опції: OFF, ERROR, WARNING, INFO, DEBUG
  18. stdout-loglevel = "DEBUG"
  19.  
  20. # Фільтр подій журналу, що використовується LoggingAdapter перед
  21. # публікацією в журнал подій до eventStream.
  22. logging-filter = "akka.event.slf4j.Slf4jLoggingFilter"
  23.  
  24. actor {
  25. provider = "akka.cluster.ClusterActorRefProvider"
  26.  
  27. default-dispatcher {
  28. # Перебіг через Dispatcher по замовчанню, 1 для максимально чесного
  29. throughput = 10
  30. }
  31. }
  32.  
  33. remote {
  34. # Порт, до якого під'єнуються клієнти. По замовчанню 2552.
  35. netty.tcp.port = 4711
  36. }
  37. }

Включення файлів

Іноді може бути корисним включити інший файл конфігурації, наприклад, якщо ви маєте одинapplication.conf з усіма незалежними від оточення налаштуваннями, та потім перекриваєте деякі налаштування для особливих оточень.

Вказавши системну властивість за допомогою-Dconfig.resource=/dev.conf завантажить файл dev.conf, що включатиме application.conf

dev.conf:

  1. include "application"
  2.  
  3. akka {
  4. loglevel = "DEBUG"
  5. }

Більш складне включення та механізми заміни пояснені в специфікації HOCON.

Журналювання конфігурації

Якщо системна властивість або конфігурація akka.log-config-on-start встановлена в on, тоді при старті системи акторів повна конфігурація скидається в журнал на рівні INFO. Це корисно, коли ви не впевнені щодо того, яка конфігурація використовується.

Якщо маєте сумніви, ви також можете просто і мило перевірити об'єкти конфігурації перед або після їх використання для конструювання системи акторів:

  1. Welcome to Scala version 2.11.7 (Java HotSpot(TM) 64-Bit Server VM, Java 1.8.0).
  2. Type in expressions to have them evaluated.
  3. Type :help for more information.
  4.  
  5. scala> import com.typesafe.config._
  6. import com.typesafe.config._
  7.  
  8. scala> ConfigFactory.parseString("a.b=12")
  9. res0: com.typesafe.config.Config = Config(SimpleConfigObject({"a" : {"b" : 12}}))
  10.  
  11. scala> res0.root.render
  12. res1: java.lang.String =
  13. {
  14. # String: 1
  15. "a" : {
  16. # String: 1
  17. "b" : 12
  18. }
  19. }

Коментарі, що передують кожному елементу, надають детальну інформацію щодо походження налаштувань (файл та номер рядка), плюс можливі присутні коментарі, тобто в відповідній конфігурації. Налаштування, що поєднані з посиланням та розібрані системою акторів можуть відображуватись таким чином:

  1. final ActorSystem system = ActorSystem.create();
  2. System.out.println(system.settings());
  3. // це скорочення до system.settings().config().root().render()

Слово щодо ClassLoaders

В декількох місцях файла конфігурації можливо вказати повністю кваліфіковане ім'я класа, що деінде створюється Akka. Це робиться через рефлексію Java, що, в свою чергу, використовує ClassLoader. Отримання потрібного в складному середовищі, як контейнери застосувань або OSGi пакунки не є завжди тривіальним, нагальний підхід Akka в тому, що кожна реалізація ActorSystem зберігає поточний завантажувач класів в контексті потоку (якщо він доступний, інакше тільки власний завентажувач, як в this.getClass.getClassLoader), та використовує його для всіх доступів через рефлексію. Це означає, що ставлячи Akka на classpath завантаження буде видаватиNullPointerException в дивних місціх: це просто не підтримується.

Специфічні до застосування налаштування

Конфігурація також може використовуватись для специфічних до застосування налаштувань. Гарною практикою є розміщення ціх налаштувань в Extension, як описано нижче:

Конфігурацію декількох ActorSystem

Якщо ви маєте більше ніж однуActorSystem (або ви пишете бібліотеку та маєте ActorSystem, що може бути відокремленою від системи акторів застосування), ви можете побажати відділити конфігурації для кожної з систем.

Приймаючи  до уваги, що ConfigFactory.load() поєднує всі ресурси зі співпадаючими іменами по всьому шляху класів, найпростіше задіяти цю функціональність, та розрізнити системи акторів в ієрархії конфігурацій:

  1. myapp1 {
  2. akka.loglevel = "WARNING"
  3. my.own.setting = 43
  4. }
  5. myapp2 {
  6. akka.loglevel = "ERROR"
  7. app2.setting = "appname"
  8. }
  9. my.own.setting = 42
  10. my.other.setting = "hello"
  1. val config = ConfigFactory.load()
  2. val app1 = ActorSystem("MyApp1", config.getConfig("myapp1").withFallback(config))
  3. val app2 = ActorSystem("MyApp2",
  4. config.getConfig("myapp2").withOnlyPath("akka").withFallback(config))

Ці два приклади демонструють різні варіації прийому “задій-піддерево”: в першому випадку конфігурація, доступна з системи акторів, подібна до такої

  1. akka.loglevel = "WARNING"
  2. my.own.setting = 43
  3. my.other.setting = "hello"
  4. // поєднання піддерев myapp1 та myapp2

тоді як в другому випадку задіяне тільки піддерево “akka” з наступним результатом

  1. akka.loglevel = "ERROR"
  2. my.own.setting = 42
  3. my.other.setting = "hello"
  4. // поєднання піддерев myapp1 та myapp2

Зауваження

Бібліотека конфігурації насправді потужна, пояслення всіх можливостей перевершує відведений тут простір. Зокрема, ми не охопили як включати файли конфігурації один в інший (дивіться простий приклад у Включенні файлв), та копіюват ичастини дерева конфігурації шляхом подміни шляху.

Ви також можете вказати та розібрати конфігурацію програмно, в інший спосіб, ніж створювати примірник ActorSystem.

  1. import akka.actor.ActorSystem
  2. import com.typesafe.config.ConfigFactory
  3. val customConf = ConfigFactory.parseString("""
  4. akka.actor.deployment {
  5. /my-service {
  6. router = round-robin-pool
  7. nr-of-instances = 3
  8. }
  9. }
  10. """)
  11. // ConfigFactory.load закладає customConfig між референсною конфігурацією
  12. // по замовчанню та перекриттями, то потім розрішує їх.
  13. val system = ActorSystem("MySystem", ConfigFactory.load(customConf))

Читання конфігурації з власного розташування

Ви можете замінити або доповнити application.conf або в коді, або використовуючи системні властивості.

Якщо ви використовуєте ConfigFactory.load() (що Akka робить по замовчанню), ви можете замінити application.conf, визначивши  -Dconfig.resource=whatever, -Dconfig.file=whatever, або   -Dconfig.url=whatever.

З середини вашого заміщаючого файла, вказаного через-Dconfig.resource та його товаришів, в можете включити include"application", якщо ви все ще бажаєте також використовуватиapplication.{conf,json,properties}. Налаштування, вказані доinclude "application" можуть бути переписані файлом включення, хоча після цього вони можуть перекрити включений файл.

В коді є багато опцій для налаштування.

Є декілька перевантаженьConfigFactory.load(); вони дозволяють вам вкуазувати дещо закладене між системними властивостями (що перекриваються), та замовчаннями (з reference.conf), заміщуючи звичайне application.{conf,json,properties} за замінюючи -Dconfig.file з друзями.

Простіший варіантConfigFactory.load() приймає ресурс (замість  application);myname.conf, myname.jsonта myname.properties після цього можуть бути задіяні замістьapplication.{conf,json,properties}.

Найбільш гнучкий варіант приймає об'єктConfig, що може бути завантажений з використанням любого методуConfigFactory. Наприклад, ви можете покласти рядок налаштування в код, використовуючи ConfigFactory.parseString(), або ви можете створити мапу, та ConfigFactory.parseMap(), або ви можете завантажити файл.

Ви також можете скомбінувати вашу власну конфігурацію зі звичайною, що може виглядати приблизно так:

  1. // зробити Config з тільки вашими особистими налаштуваннями
  2. Config myConfig =
  3. ConfigFactory.parseString("something=somethingElse");
  4. // завантажити звичайний стек конфігурації (системні властивості,
  5. // потім application.conf, потім reference.conf)
  6. Config regularConfig =
  7. ConfigFactory.load();
  8. // перекриваємо звичайний стек нашим myConfig
  9. Config combined =
  10. myConfig.withFallback(regularConfig);
  11. // покласти результат посередині між перекриттями
  12. // (системними властивостями) та замовченнями
  13. Config complete =
  14. ConfigFactory.load(combined);
  15. // створюємо ActorSystem
  16. ActorSystem system =
  17. ActorSystem.create("myname", complete);

При роботі з об'єктами Config, тримайте на увазі, що в цьому торті є три "прошарка":

  • ConfigFactory.defaultOverrides() (системні властивості)
  • налаштування застосування
  • ConfigFactory.defaultReference() (файл reference.conf)

Звичайною ціллю є налаштування проміжного слою, тоді як інші два залишаються як є.

  • ConfigFactory.load() завантажує повний стек
  • перекриття з ConfigFactory.load() дозволяє вам вказати відмінний проміжний прошарок
  • варіації ConfigFactory.parse() завантажують окремі файли та ресурси

Щоб накласти два прошарка використовуйтеoverride.withFallback(fallback); спробуйте утримувати системні властивості (defaultOverrides()) зверху, та reference.conf (defaultReference()) знизу.

Майте на увазі, що часто ви можете тільки додати твердження include до application.conf, скоріше, ніж писати код. Вставлення на початку  application.conf будуть перекриті залишком application.conf, тоді як вставки наприкінці будуть  перекривати ранішні налаштування.

Конфігурація розгортання актора

Налаштування розгортання для окремих акторів може бути визначена в розділі конфігураціїakka.actor.deployment. В розділі розгортання можливо визначити такі речі, як диспечер, поштова скринька, налаштування маршрутизатора та віддалене розгортання. Конфігурація ціх можливостей описана в главах, де розкриті відповідні теми. Приклад може виглядати так:

  1. akka.actor.deployment {
  2.  
  3. # '/user/actorA/actorB' є віддалено розміщеним актором
  4. /actorA/actorB {
  5. remote = "akka.tcp://sampleActorSystem@127.0.0.1:2553"
  6. }
  7. # всі прямі діти '/user/actorC' мають виділений диспечер
  8. "/actorC/*" {
  9. dispatcher = my-dispatcher
  10. }
  11. # '/user/actorD/actorE' має окремий приоритет поштової скриньки
  12. /actorD/actorE {
  13. mailbox = prio-mailbox
  14. }
  15. # '/user/actorF/actorG/actorH' є випадковим пулом
  16. /actorF/actorG/actorH {
  17. router = random-pool
  18. nr-of-instances = 5
  19. }
  20. }
  21.  
  22. my-dispatcher {
  23. fork-join-executor.parallelism-min = 10
  24. fork-join-executor.parallelism-max = 10
  25. }
  26. prio-mailbox {
  27. mailbox-type = "a.b.MyPrioMailbox"
  28. }

Зауваження

Розділ розгорнення для окремого актора ідентифікується шляхом актора відповідно до /user.

Ви можете використовувати зірочки для розділів шляху актора, так що ви можете вказати: /*/sampleActor, та це може зпівпадати з усімаsampleActor на цьому рівні ієрархії. Ви також можете використовувати зірочки в останній позиції, для співпадіння з усіма акторами на певному рівні: /someParent/*. Не-узагальнені співпадіння завжди мають вищий приоритет, ніж співпадіння з зірочками, так що/foo/bar буде визнаний більш специфічним, ніж /foo/*, та буде використаний тільки більш вищий приоритет співпадіння. будь ласка, зауважте, що це не може бути використане для розділу часткового співпадіня, як це: /foo*/bar, /f*o/bar та подібне до цього.

Перегляд референсної конфігурації

Кожний модуль Akka має файл референсної конфігурації зі значеннями за замовченням.

akka-actor

  1. ####################################
  2. # Akka Actor Reference Config File #
  3. ####################################
  4.  
  5. # Це референсний файл конфігурації, що містить всі налаштування по замвчанню.
  6. # Робіть ваші редакції/перекриття в вашому application.conf.
  7.  
  8. akka {
  9. # Версія Akka, відповідно до робочої версії Akka.
  10. version = "2.4.1"
  11.  
  12. # Домашній каталог Akka, модулі в цьому каталозі будуть завантажені
  13. home = ""
  14.  
  15. # Логери, що реєструються під час завантаження
  16. # (akka.event.Logging$DefaultLogger пише до STDOUT)
  17. loggers = ["akka.event.Logging$DefaultLogger"]
  18. # Фільтр подій журналу, що використовується LoggingAdapter перед
  19. # публікацією подій журналу до eventStream. Він може виконувати
  20. # добре виважене фільтрування на основі витоку повідомлень. По замовчанню
  21. # реалізація фільтрує на основі `loglevel`.
  22. # FQCN LoggingFilter. Class FQCN мусить реалізувати
  23. # akka.event.LoggingFilter та мати публічний конструктор з параметрами
  24. # (akka.actor.ActorSystem.Settings, akka.event.EventStream).
  25. logging-filter = "akka.event.DefaultLoggingFilter"
  26.  
  27. # Логери, створювані та зареєстровані синхронно під час запуску ActorSystem,
  28. # та оскільки це актори, цей таймаут використовується для обмеження
  29. # часу очікування
  30. logger-startup-timeout = 5s
  31.  
  32. # Рівень журналу, що використовується для сконфігурованих логерів
  33. # як тільки вони стартують; перед цім дивіться "stdout-loglevel"
  34. # Опції: OFF, ERROR, WARNING, INFO, DEBUG
  35. loglevel = "INFO"
  36.  
  37. # Рівень журналу для дуже базового логера, активованого під час запуску
  38. # ActorSystem. Цей логер друкує повідомлення журналу до stdout (System.out).
  39. # Опції: OFF, ERROR, WARNING, INFO, DEBUG
  40. stdout-loglevel = "WARNING"
  41.  
  42. # Журналює повну конфігурацію на рівні INFO, коли стартує система акторів.
  43. # Це корисне, коли ви не впевнені, яка конфігурація використовується.
  44. log-config-on-start = off
  45.  
  46. # Журналює на рівні info, коли повідомлення надсилаються до мертвих листів.
  47. # Можливі значення:
  48. # on: журналюються всі мертві повідомлення
  49. # off: мертві повідомлення не журналюються
  50. # n: позитивне ціле, число мертвих листів, що будуть журналюватись
  51. log-dead-letters = 10
  52.  
  53. # Можливість відключити журналювання мертвих листів під час завершення
  54. # системи акторів. Журналювання виконується тільки коли включене
  55. # налаштування 'log-dead-letters'.
  56. log-dead-letters-during-shutdown = on
  57.  
  58. # Список FQCN розширень, що будуть завантажені при запуску системи акторів.
  59. # Має бути в форматі: 'extensions = ["foo", "bar"]' тощо.
  60. # Дивіться документацію Akka щодо додаткових даних про розширення
  61. extensions = []
  62.  
  63. # Перемикає, чи потоки, створені цією ActorSystem будуть демонами
  64. daemonic = off
  65.  
  66. # Завершення JVM, System.exit(-1) в випадку фатальної помилки,
  67. # такої, як OutOfMemoryError
  68. jvm-exit-on-fatal-error = on
  69.  
  70. actor {
  71.  
  72. # FQCN до задіяного ActorRefProvider; нижче наведене значення по замовчанню,
  73. # інше значення akka.remote.RemoteActorRefProvider з пакунку akka-remote.
  74. provider = "akka.actor.LocalActorRefProvider"
  75.  
  76. # Охоронець "/user" буде використовувати цей клас для отримання supervisorStrategy.
  77. # Це має бути субклас akka.actor.SupervisorStrategyConfigurator.
  78. # На додаток до замовчання є akka.actor.StoppingSupervisorStrategy.
  79. guardian-supervisor-strategy = "akka.actor.DefaultSupervisorStrategy"
  80.  
  81. # Таймаут для ActorSystem.actorOf
  82. creation-timeout = 20s
  83.  
  84. # Серіалізотори та десеріалізатори (не примітивні) повідомлень,
  85. # щоб впевнитись щодо незміності. Призначене тільки для тестування.
  86. serialize-messages = off
  87.  
  88. # Створювання серіалізаторів та десеріалізаторів (в Props), щоб переконатись,
  89. # що вони можуть бути передані по мережі. Це призначено тільки для тестування.
  90. # Чисто локальні розгортання, помічені як deploy.scope == LocalScope
  91. # виключаються з перевірки.
  92. serialize-creators = off
  93.  
  94. # Таймаут для операцій надсилання до акторів вищого рівня, що є в процесі
  95. # стартування. Це має відношення тільки в випадку, якщо використовується
  96. # прив'язана поштова скринька або theCallingThreadDispatcher
  97. # для акторів вищого рівня.
  98. unstarted-push-timeout = 10s
  99.  
  100. typed {
  101. # Таймаут по замовчанню для типованих методів актора з непустим
  102. # типом результата
  103. timeout = 5s
  104. }
  105. # Відзеркалення коротких імен ´deployment.router' на повністю
  106. # кваліфіковані імена класів
  107. router.type-mapping {
  108. from-code = "akka.routing.NoRouter"
  109. round-robin-pool = "akka.routing.RoundRobinPool"
  110. round-robin-group = "akka.routing.RoundRobinGroup"
  111. random-pool = "akka.routing.RandomPool"
  112. random-group = "akka.routing.RandomGroup"
  113. balancing-pool = "akka.routing.BalancingPool"
  114. smallest-mailbox-pool = "akka.routing.SmallestMailboxPool"
  115. broadcast-pool = "akka.routing.BroadcastPool"
  116. broadcast-group = "akka.routing.BroadcastGroup"
  117. scatter-gather-pool = "akka.routing.ScatterGatherFirstCompletedPool"
  118. scatter-gather-group = "akka.routing.ScatterGatherFirstCompletedGroup"
  119. tail-chopping-pool = "akka.routing.TailChoppingPool"
  120. tail-chopping-group = "akka.routing.TailChoppingGroup"
  121. consistent-hashing-pool = "akka.routing.ConsistentHashingPool"
  122. consistent-hashing-group = "akka.routing.ConsistentHashingGroup"
  123. }
  124.  
  125. deployment {
  126.  
  127. # id шаблону розгортання в форматі /parent/child etc.
  128. default {
  129. # Це id диспечера, що буде використовуватись з цім актором.
  130. # Якщо воно не визначене або пусте, диспечер, використовується вказаний
  131. # в коді (Props.withDispatcher), або default-dispatcher, якщо
  132. # взагалі не вказане.
  133. dispatcher = ""
  134.  
  135. # Це id поштової скриньки, що буде використовуватись з цім актором.
  136. # Якщо воно не визначене або пусте, поштова буде використана скринька
  137. # по замовчанню сконфігурованого диспечера, або, якщо поштова скринька не
  138. # задана в конфігурації, буде використана поштова скринька в коді
  139. # (Props.withMailbox). Якщо є скринька, визначена в сконфігурованому
  140. # диспечері, тоді вона перекриватиме це налаштування.
  141. mailbox = ""
  142.  
  143. # схема маршрутизації (наланс навантаження), що буде задіяна
  144. # - доступні: "from-code", "round-robin", "random", "smallest-mailbox",
  145. # "scatter-gather", "broadcast"
  146. # - або Повністю кваліфіковане ім'я класу маршрутизатора.
  147. # Клас має розширювати akka.routing.CustomRouterConfig та
  148. # мати публічний конструктор з com.typesafe.config.Config
  149. # та опціональним праметром akka.actor.DynamicAccess.
  150. # - по замовчанню "from-code";
  151. # Чи буде актор трансформовано на маршрутизатор, визначаєтсья в коді
  152. # (Props.withRouter). Тип машрутизатора може бути перекритий в
  153. # конфігурації; вказуючи "from-code" означає, що будуть задіяні значення,
  154. # вказані в коді.
  155. # В випадку маршрутизації актори будуть машрутуватись в декілька способів:
  156. # - nr-of-instances: буде створювати стільки дітей
  157. # - routees.paths: буде маршрутизувати повідомлення по цьому шляху
  158. # з використанням ActorSelection, тобто не створюватиме дітей
  159. # - resizer: динамічно змінюване число маршрутів, як вказане нижче
  160. router = "from-code"
  161.  
  162. # число дітей, що створюються в випадку маршрутизатора;
  163. # це налаштування ігнорується, якщо вказаний routees.paths
  164. nr-of-instances = 1
  165.  
  166. # таймаут, використовуваний для маршрутизаторів, що містять
  167. # майбутні виклики
  168. within = 5 seconds
  169.  
  170. # число віртуальних вузлів на один вузол, для маршрутизатора
  171. # consistent-hashing
  172. virtual-nodes-factor = 10
  173.  
  174. tail-chopping-router {
  175. # інтервал є відтинком часу для пересилання на наступний маршрут
  176. interval = 10 milliseconds
  177. }
  178.  
  179. routees {
  180. # Альтернативно для надання nr-of-instances, як ви можете вказати
  181. # повні шляхи до тих акторів, до яких потрібна маршрутизація.
  182. # Це налаштування має перевагу над nr-of-instances
  183. paths = []
  184. }
  185. # Щоб використати окремий диспечер для маршрутів пула, ви можете
  186. # визначити конфігурацію диспечера поряд з ім'ям властивості
  187. # 'pool-dispatcher' в розділі розгортання маршрутизатора.
  188. # Наприклад:
  189. # pool-dispatcher {
  190. # fork-join-executor.parallelism-min = 5
  191. # fork-join-executor.parallelism-max = 5
  192. # }
  193.  
  194. # Маршрутизатори з динамічним числом маршрутів; ця можливість
  195. # включена через включення (частин) цього розділу до розгортання
  196. resizer {
  197. enabled = off
  198.  
  199. # Найменьше число маршрутів, що має утримувати маршутизатор.
  200. lower-bound = 1
  201.  
  202. # Найбільше число маршрутів, що має утримувати маршрутизатор.
  203. # Має бути більше або рівне lower-bound.
  204. upper-bound = 10
  205.  
  206. # Рівень відсікання, що використовується для обчислення, чи маршрут
  207. # слід визнати навантаженим (під тиском). Реалізації залежать від
  208. # цього значення (по замовчанню 1).
  209. # 0: число маршрутів, що наразі обробляють повідомлення.
  210. # 1: число маршрутів, що наразі мають деякі повідомлення в
  211. # поштовій скриньці.
  212. # > 1: число маршрутів, з щонайменьше сконфігурованим в
  213. # pressure-thresholdmessages числом повідомлень в поштовій скриньці.
  214. # Зауважте, що очікуваний розмір скриньки UnboundedMailbox є
  215. # операцією O(N).
  216. pressure-threshold = 1
  217.  
  218. # Відсоток розміру зростання, коли всі маршрути зайняті.
  219. # Наприклад, 0.2 буде давати зростання 20% (округлене доверху),
  220. # тобто якщо поточна ємність 6, це дасть запит на на 2 додаткові маршрути.
  221. rampup-rate = 0.2
  222.  
  223. # Мінімальна доля зайнятих маршрутів, перед відмовою від маршрутів.
  224. # Наприклад, якщо це 0.3, тоді ми видаляємодеяки маршрути, коли
  225. # меньше ніж 30% маршрутів зайняті, тобто, якщо поточна ємність є 10,
  226. # та 3 зайняті, тоді ємність не змінюється, але якщо зайняті 2 або меньше,
  227. # тоді ємність буде зменшена.
  228. # Використовуйте 0.0 або від'ємне значення, щоб уникнути видалення
  229. # маршрутів.
  230. backoff-threshold = 0.3
  231.  
  232. # Доля маршрутів, що будуть видалені, коли їх число досягне
  233. # backoff-Threshold.
  234. # Наприклад, 0.1 буде зменьшувати на 10% (округлене), тобто, якщо
  235. # поточна місткість 9, буде запит на зменшення 1 маршруту.
  236. backoff-rate = 0.1
  237.  
  238. # Число повідомлень між операціями зменшення.
  239. # Використовуйте 1, щоб змінювати розмір перед кожним повідомленням.
  240. messages-per-resize = 10
  241. }
  242.  
  243. # Маршрутизатори зі змінним числом маршрутів на основі
  244. # метрик продуктивності.
  245. # Ця можливість доступна черезвключення (частин) цього розділу в
  246. # розгортання, не може бути включене разом з підлаштуванням розміру
  247. # по замовчанню.
  248. optimal-size-exploring-resizer {
  249.  
  250. enabled = off
  251.  
  252. # Найменьше число маршрутів, що повинен мати маршрутизатор.
  253. lower-bound = 1
  254.  
  255. # Найбільше число маршрутів, що повинен мати маршрутизатор.
  256. # Має бути більше або рівне lower-bound.
  257. upper-bound = 10
  258.  
  259. # Вірогідність виконання скочування, коли всі маршрути зайняті
  260. # під час дослідшення.
  261. chance-of-ramping-down-when-full = 0.2
  262.  
  263. # Інтервал між кожною спробою зміни розміра
  264. action-interval = 5s
  265.  
  266. # Якщо маршрути не були повністю використані (тобто не всі зайняті)
  267. # для такої довжини, відбудеться спроба зменшити пул.
  268. downsize-after-underutilized-for = 72h
  269.  
  270. # Дослідження тривалості, відношення між найбільшим кроком, та
  271. # поточним розміром пула. Тобто, якщо поточний пул має розмір 50,
  272. # та explore-step-size 0.1, максимальний розмір зміни пула на протязі
  273. # дослідження буде +- 5
  274. explore-step-size = 0.1
  275.  
  276. # Вірогідність виконання дослідження, або оптимізації.
  277. chance-of-exploration = 0.4
  278.  
  279. # Коли відбувається скорочення після довгого недовикористання,
  280. # процес буде зменшувати пул до найбільшого вуикористання, помножене
  281. # на коефіцієнт. Цей коефіцієнт зменшення визначає новий розмір пула
  282. # в порівнянні з найбільшим навантаженням.
  283. # Тобто, якщо найбільше навантаження 10, та коефіціент зменшення
  284. # є 0.8, пул буде зменшений до 8
  285. downsize-ratio = 0.8
  286.  
  287. # При оптимізації береться до уваги тільки зміну до поточного розміру.
  288. # Це число вказує, скільки поточних розмірів буде прийматись до уваги.
  289. optimization-range = 16
  290.  
  291. # Вага останньої метрики над старими метриками при зборі метрик
  292. # продуктивності.
  293. # Тобто, якщо остання швидкість обробки є 10мс на повідомлення,
  294. # з роміром пула 5, та якщо нова зібрана швидкість обробки 6мс
  295. # на повідомлення при розмірі пула 5. Тоді беручи вагу 0.3, метріка,
  296. # що представляє пул розміром 5 буде 6 * 0.3 + 10 * 0.7, тобто 8.8мс.
  297. # Очевидно, це число має бути між 0 та 1.
  298. weight-of-latest-metric = 0.5
  299. }
  300. }
  301.  
  302. /IO-DNS/inet-address {
  303. mailbox = "unbounded"
  304. router = "consistent-hashing-pool"
  305. nr-of-instances = 4
  306. }
  307. }
  308.  
  309. default-dispatcher {
  310. # Має бути одне з наступного:
  311. # Dispatcher, PinnedDispatcher, або FQCN до наслідуючого класу
  312. # MessageDispatcherConfigurator з публічним конструктором з параметрами
  313. # com.typesafe.config.Config та akka.dispatch.DispatcherPrerequisites.
  314. # PinnedDispatcher має бути використаний разом з executor=thread-pool-executor.
  315. type = "Dispatcher"
  316.  
  317. # Який різновид ExecutorService використовувати з цім диспечером
  318. # Прийнятні опції:
  319. # - "default-executor" протребує розділу "default-executor"
  320. # - "fork-join-executor" протребує розділу "fork-join-executor"
  321. # - "thread-pool-executor" потребує розділу "thread-pool-executor"
  322. # - FQCN класу, що розширює ExecutorServiceConfigurator
  323. executor = "default-executor"
  324.  
  325. # Це буде використовуватись, якщо ви встановили"executor="default-executor"".
  326. # Якщо ActorSystem створена з наданим ExecutionContext, цей
  327. # ExecutionContext буде використаний як екзекутор по замовчанню для всіх
  328. # диспечерів в ActorSystem, сконфігурованого для executor="default-executor".
  329. # Зауважте, що "default-executor" є значенням по замовчанню для екзекутора,
  330.  # та, таким чином, використовується, якщо не вказане інакше.
  331. # Якщо не надано ExecutionContext, використовується екзекутор,
  332. # сконфігурований як "fallback".
  333. default-executor {
  334. fallback = "fork-join-executor"
  335. }
  336.  
  337. # Це буде використане, якщо ви встановили "executor="fork-join-executor""
  338. fork-join-executor {
  339. # Мінімальне число потоків, щоб перевершіти factor-based паралелізм
  340. parallelism-min = 8
  341.  
  342. # Фактор паралелізму, що використовується для визначеннярозміру пула потоків
  343. # з використанням наступної формули: ceil(available processors * factor).
  344. # Отриманий розмір потім обмежується parallelism-min та parallelism-max.
  345. parallelism-factor = 3.0
  346.  
  347. # Максимальне число потоків, щоб покрити фактор паралелізму
  348. parallelism-max = 64
  349.  
  350. # Встановлюється в "FIFO", щоб встановити режим черги, "poll" або "LIFO"
  351. # для використання дисціпліни стеку, що може "pop".
  352. task-peeking-mode = "FIFO"
  353. }
  354.  
  355. # Це використовується, якщо встановлене "executor = "thread-pool-executor""
  356. thread-pool-executor {
  357. # Час Keep-alive time для потоків
  358. keep-alive-time = 60s
  359.  
  360. # Мінімальне число потоків, щоб обмежити фактор числа ядер
  361. core-pool-size-min = 8
  362.  
  363. # Фактор розміру пула ядер, використовується для визначення розміру
  364. # потоків ядра за такою формулою: ceil(доступні_процесори*фактор).
  365. # Отриманий розміробмежується значеннями core-pool-size-min та
  366. # core-pool-size-max.
  367. core-pool-size-factor = 3.0
  368.  
  369. # Максимальне число потоків, щоб обмежити фактор числа ядер
  370. core-pool-size-max = 64
  371.  
  372. # Мінімальне число потоків, щоб обмежити фактор максимального числа
  373. # (якщо використовується обмежена черга завдань)
  374. max-pool-size-min = 8
  375.  
  376. # Максимальне число потоків (якщо використовується обмежена черга завдань)
  377. # що обчислюється таким чином: ceil(available_processors*factor)
  378. max-pool-size-factor = 3.0
  379.  
  380. # Максимальне число потоків, щоб обежити число фактор числа потоків
  381. # (якщо використовується обмежена черга завдань)
  382. max-pool-size-max = 64
  383.  
  384. # Вказує обмежену місткість чарги завдать (< 1 == необмежено)
  385. task-queue-size = -1
  386.  
  387. # Вказує, який тип черги завдань використовувати, може бути "array" або
  388. # "linked" (по замовчанню)
  389. task-queue-type = "linked"
  390.  
  391. # Дозволяє таймаут потоків ядра
  392. allow-core-timeout = on
  393. }
  394.  
  395. # Як багато часу буде чекати диспечер нового актора, доки він завершиться
  396. shutdown-timeout = 1s
  397.  
  398. # Пропускна здібність. Число повідомлень,що обробляються пакетом,
  399. # перед тим, як потік повернеться до пула. 1 означає максимальну чесність.
  400. throughput = 5
  401.  
  402. # Останній строк для диспечера, 0 або від'ємне означає відсутність
  403. throughput-deadline-time = 0ms
  404.  
  405. # Для BalancingDispatcher: якщо балансуючий диспечер повинен спробувати
  406. # планувати байдикуючі актори з тим же диспечером, коли надходить
  407. # повідомлення, та диспечери ExecutorService ще не повністю навантажені.
  408. attempt-teamwork = on
  409.  
  410. # Якщо диспечер потребує особливого типу поштової скриньки, тут вказується
  411. # FQCN; насправді створена поштова скринька буде підтипом цього типа.
  412. # Порожній рядок вказує, що можливість не потрібна.
  413. mailbox-requirement = ""
  414. }
  415.  
  416. default-mailbox {
  417. # FQCN MailboxType. Клас FQCN мусить мати публічний конструктор з
  418. # параметрами (akka.actor.ActorSystem.Settings, com.typesafe.config.Config).
  419. mailbox-type = "akka.dispatch.UnboundedMailbox"
  420.  
  421. # Коли поштова скринька обмежена, вона використовує це налаштування для
  422. # визначення власної ємності. Запроваджене значення має бути додатним.
  423. # ЗАУВАЖЕННЯ:
  424. # До версії 2.1 тип поштової скриньки визначався на основі цього налаштування;
  425. # тепер це не так, тип треба вказувати явно як обмежена поштова скринька
  426. mailbox-capacity = 1000
  427.  
  428. # Якщо скринька обмежена, тоді цей таймаут для ставлення в чергу, коли
  429. # скринька повна. Від'ємні значення вказують безкінечний таймаут,
  430. # що треба уникати, бо це викликає ризик ситуації глухого кута (зависання).
  431. mailbox-push-timeout-time = 10s
  432.  
  433. # Для актора зі Stash: Ємність сховку по замовчанню.
  434. # Якщо від'ємне (або нуль), тоді використовується необмежений сховок
  435. # (по замовчанню), якщо додатне, тоді сховок обмежений, та ємність
  436. # визначається значенням цієї властивості
  437. stash-capacity = -1
  438. }
  439.  
  440. mailbox {
  441. # Відображення між семантикою черги повідомлень та конфігурацією скриньки.
  442. # Використовується akka.dispatch.RequiresMessageQueue[T] для астановлення
  443. # різних типів поштових скриньок для акторів.
  444. # Якщо ваш Actor реалізує RequiresMessageQueue[T], тоді при створенні
  445. # примірника такого актора тип його скриньки буде визначатись з огляду
  446. # на конфігурацію поштової скриньки через T в цій мапі
  447. requirements {
  448. "akka.dispatch.UnboundedMessageQueueSemantics" =
  449. akka.actor.mailbox.unbounded-queue-based
  450. "akka.dispatch.BoundedMessageQueueSemantics" =
  451. akka.actor.mailbox.bounded-queue-based
  452. "akka.dispatch.DequeBasedMessageQueueSemantics" =
  453. akka.actor.mailbox.unbounded-deque-based
  454. "akka.dispatch.UnboundedDequeBasedMessageQueueSemantics" =
  455. akka.actor.mailbox.unbounded-deque-based
  456. "akka.dispatch.BoundedDequeBasedMessageQueueSemantics" =
  457. akka.actor.mailbox.bounded-deque-based
  458. "akka.dispatch.MultipleConsumerSemantics" =
  459. akka.actor.mailbox.unbounded-queue-based
  460. "akka.dispatch.ControlAwareMessageQueueSemantics" =
  461. akka.actor.mailbox.unbounded-control-aware-queue-based
  462. "akka.dispatch.UnboundedControlAwareMessageQueueSemantics" =
  463. akka.actor.mailbox.unbounded-control-aware-queue-based
  464. "akka.dispatch.BoundedControlAwareMessageQueueSemantics" =
  465. akka.actor.mailbox.bounded-control-aware-queue-based
  466. "akka.event.LoggerMessageQueueSemantics" =
  467. akka.actor.mailbox.logger-queue
  468. }
  469.  
  470. unbounded-queue-based {
  471. # FQCN MailboxType, клас FQCN мусить мати публічний конструктор
  472. # з параметрами (akka.actor.ActorSystem.Settings,
  473. # com.typesafe.config.Config).
  474. mailbox-type = "akka.dispatch.UnboundedMailbox"
  475. }
  476.  
  477. bounded-queue-based {
  478. # FQCN MailboxType, клас FQCN мусить мати публічний конструктор з
  479. # параметрами (akka.actor.ActorSystem.Settings,
  480. # com.typesafe.config.Config).
  481. mailbox-type = "akka.dispatch.BoundedMailbox"
  482. }
  483.  
  484. unbounded-deque-based {
  485. # FQCN MailboxType, клас FQCN мусить мати публічний конструктор з
  486. # параметрами (akka.actor.ActorSystem.Settings,
  487. # com.typesafe.config.Config).
  488. mailbox-type = "akka.dispatch.UnboundedDequeBasedMailbox"
  489. }
  490.  
  491. bounded-deque-based {
  492. # FQCN MailboxType, клас FQCN мусить мати публічний конструктор з
  493. # параметрами (akka.actor.ActorSystem.Settings,
  494. # com.typesafe.config.Config).
  495. mailbox-type = "akka.dispatch.BoundedDequeBasedMailbox"
  496. }
  497.  
  498. unbounded-control-aware-queue-based {
  499. # FQCN MailboxType, клас FQCN мусить мати публічний конструктор з
  500. # параметрами (akka.actor.ActorSystem.Settings,
  501. # com.typesafe.config.Config).
  502. mailbox-type = "akka.dispatch.UnboundedControlAwareMailbox"
  503. }
  504.  
  505. bounded-control-aware-queue-based {
  506. # FQCN MailboxType, клас FQCN мусить мати публічний конструктор з
  507. # параметрами (akka.actor.ActorSystem.Settings,
  508. # com.typesafe.config.Config).
  509. mailbox-type = "akka.dispatch.BoundedControlAwareMailbox"
  510. }
  511. # LoggerMailbox буде вичерпувати всі повідомлення в поштовій скриньці
  512. # при завершенні системи, та доставляти їх до StandardOutLogger.
  513. # Не змінюйте це, якщо не знаєте, що робите.
  514. logger-queue {
  515. mailbox-type = "akka.event.LoggerMailboxType"
  516. }
  517. }
  518.  
  519. debug {
  520. # вмикає функцію Actor.loggable(), що журналює всі отримані повідомлення
  521. # на рівні DEBUG, дивіться розділ “тестування системи акторів” документації
  522. # Akka на http://akka.io/docs
  523. receive = off
  524.  
  525. # вмикає журналювання DEBUG всіх AutoReceiveMessages (Kill, PoisonPill etc)
  526. autoreceive = off
  527.  
  528. # вмикає журналювання DEBUG змін життєвого циклу акторів
  529. lifecycle = off
  530.  
  531. # вмикає журналювання DEBUG всіх LoggingFSM для подій, переходів та таймерів
  532. fsm = off
  533.  
  534. # вмикає журналювання DEBUG змін підписки на eventStream
  535. event-stream = off
  536.  
  537. # вмикає журналювання DEBUG необроблених повідомлень
  538. unhandled = off
  539.  
  540. # вмикає журналювання WARN невірно сконфігурованих маршрутів
  541. router-misconfiguration = off
  542. }
  543.  
  544. # Входження для підключуваних серіалізаторів та їх прив'язок.
  545. serializers {
  546. java = "akka.serialization.JavaSerializer"
  547. bytes = "akka.serialization.ByteArraySerializer"
  548. }
  549.  
  550. # Клас для прив'язок Serializer. Вам треба вказати тільки ім'я інтерфейса
  551. # або абстрактного базового класу повідомлень. В випадку неоднозначності
  552. # використовується найбільш специфічний сконфігурований клас, або видається
  553. # попередження, що обирається “перший зустрічний”.
  554. #
  555. # Щоб відключити один з серіалізаторів по замовчанню, встановіть його клас
  556. # в "none", як в "java.io.Serializable" = none
  557. serialization-bindings {
  558. "[B" = bytes
  559. "java.io.Serializable" = java
  560. }
  561.  
  562. # Журнал попереджень, коли серіалізація Java по замовчаннювикористовується для
  563. # серіалізації повідомлень. Серіалізатор по замовчанню використовує Java, що
  564. # не дуже оптимально, та не повинно використовуватись в виробничому оточенні,
  565. # тільки якщо вам не байдужа продуктивність. В такому випадку ви можете
  566. # перемикнути це в off.
  567. warn-about-java-serializer-usage = on
  568.  
  569. # Конфігурація простору імен ідентифікаторів сериалізації.
  570. # Кожна реалізація сериалізації повинна мати входження в наступному форматі:
  571. # `akka.actor.serialization-identifiers."FQCN" = ID`
  572. # де `FQCN` повністю кваліффіковане ім'я класа реалізації сериалізації,
  573. # та `ID` є глобально унікальне число-ідентифікатор серіалізатора.
  574. # Значення ідентифікаторів починаючи з 0 до 16 зарезервовані для
  575. # внутрішнього використання Akka.
  576. serialization-identifiers {
  577. "akka.serialization.JavaSerializer" = 1
  578. "akka.serialization.ByteArraySerializer" = 4
  579. }
  580.  
  581. # Елементи конфігурації, що використовуються методами akka.actor.ActorDSL._
  582. dsl {
  583. # Максимальний розмір черги акторів, створених newInbox(); це захищає
  584. # проти збойних програм, що використовують select(), та постійно втрачають
  585. # повідомлення
  586. inbox-size = 1000
  587.  
  588. # Таймаут по замовчанню для опкерацій як Inbox.receive, та інших
  589. default-timeout = 5s
  590. }
  591. }
  592.  
  593. # Використовується для встановлення поведінки планувальника.
  594. # Зміна значень по замовчанню може значно змінити поведінку системи, так що
  595. # переконайтесь, що ви знаєте, що робите! Дивіться розділ Scheduler документації
  596. # Akka Documentation щодо деталей.
  597. scheduler {
  598. # LightArrayRevolverScheduler використовується в системі як планувальник по замовчанню.
  599. # Він не виконує завдання в певний час, але кожиний тік він виконує все,
  600. # що накопичилось. Ви можете збільшити або зменшити точність часу виконання,
  601. # вказуючі більші або меньші проміжки часу тіків. Якщо ви плануєте багато
  602. # завдань, ви можете розглянути можливість збільшення тіків на оберт.
  603. # Зауважте, що може пройти 1 тік для зупинки таймера, так що встановлення
  604. # тривалості тіка в більше значення зробить завершення системи акторів
  605. # більш тривалим.
  606. tick-duration = 10ms
  607.  
  608. # Таймер використовує циклічний оберт пачок, щоб зберігати завдання таймера.
  609. # Це має бути встановлене таким чином, щоб більшість таймаутів планування
  610. # (для високої частоти планування) було б коротше, ніж один оберт
  611. # (ticks-per-wheel * ticks-duration)
  612. # ЦЕ МАЄ БУТИ СТУПІНЬ ДВОХ!
  613. ticks-per-wheel = 512
  614.  
  615. # Це налаштування обирає реалізацію таймера, що буде завантажена системою
  616. # при запуску.
  617. # Наданий тут клас має реалізовувати інтерфейс akka.actor.Scheduler
  618. # та пропонувати публічний конструктор, що приймає три аргументи:
  619. # 1) com.typesafe.config.Config
  620. # 2) akka.event.LoggingAdapter
  621. # 3) java.util.concurrent.ThreadFactory
  622. implementation = akka.actor.LightArrayRevolverScheduler
  623.  
  624. # При завершенні планувальника типово є потік, що треба зупинити, та цей
  625. # таймаут визначає, як довго чекати, коли це трапиться. В випадку таймауту
  626. # завершення система акторів буде продовжувати без виконання, можливо все
  627. # ставлячи завдання в чергу.
  628. shutdown-timeout = 5s
  629. }
  630.  
  631. io {
  632.  
  633. # По замовчанню цикли select виконуються в виділених потоках, таким чином
  634. # використовуючи PinnedDispatcher
  635. pinned-dispatcher {
  636. type = "PinnedDispatcher"
  637. executor = "thread-pool-executor"
  638. thread-pool-executor.allow-core-timeout = off
  639. }
  640.  
  641. tcp {
  642.  
  643. # Число селекторів для очищення обслуговуваних каналів; кожний з них
  644. # буде використовувати один цикл select на selector-dispatcher.
  645. nr-of-selectors = 1
  646.  
  647. # Максимальне число відкритих каналів, підтримуваних цім TCP модулем;
  648. # не існує внутрішнього загального ліміту, це налаштування призначене
  649. # для запобігання DoS, обмежуючи число конкурентно під'єднаних клієнтів.
  650. # Також зауважте, що це "софт" ліміт; в деяких випадках реалізація
  651. # буде сприймати на декілька з'єднань більше, або на декілька меньше,
  652. # ніж сконфігуроване тут число. Має бути цілим > 0 або "unlimited".
  653. max-channels = 256000
  654.  
  655. # При спробі призначити нове з'єднання до селектора, та цей селектор
  656. # вичерпав ємність, спробувати новий селекто, та присвоювати стільки
  657. # разів перед тим, як облишити спроби
  658. selector-association-retries = 10
  659.  
  660. # Максимальне число з'єднань, що сприймаються за один раз,
  661. # вищі значення зменшують затримки, нижчі значення збільшують чесність
  662. # на worker-dispatcher
  663. batch-accept-limit = 10
  664.  
  665. # Число байт на один прямий буфер в пулі, що використовується для
  666. # читання або запису даних з ядра.
  667. direct-buffer-size = 128 KiB
  668.  
  669. # Максимальне число різних буферів, що утримуються в прямому пулі буферів
  670. # для повторного використання.
  671. direct-buffer-pool-limit = 1000
  672.  
  673. # Проміжок часу, що актор з'єднання чекає повідомлення `Register` від
  674. # його командуючого, перед обірванням з'єднання.
  675. register-timeout = 5s
  676.  
  677. # Максимальне число байтів, доставлених в повідомленні `Received`. Перед
  678. # тим, як читати додаткові дані з мережі, актор з'єднання спробує
  679. # робити іншу роботу.
  680. # Призначення цього налаштування є надати меньший ліміт, ніж сконфігурований
  681. # розмір буфера отримання. Якщо використовується значення 'unlimited'
  682. # буде спроба прочитати всі дані з вхідного буфера.
  683. max-received-message-size = unlimited
  684.  
  685. # Дозволяє гарно налаштувати журналювання того, що відбувається в реалізації.
  686. # Будьте уважні, це може створювати більше одного запису на повідомлення,
  687. # надісланого до акторів через tcp реалізацію.
  688. trace-logging = off
  689.  
  690. # Повністю кваліфікований шлях конфігурації, що містить конфігурацію
  691. # маршрутизатора для виконання викликів select() в селекторах
  692. selector-dispatcher = "akka.io.pinned-dispatcher"
  693.  
  694. # Повнісню кваліфікований шлях конфігурації, що містить конфігурацію
  695. # диспечера для робочих акторів читання/запису
  696. worker-dispatcher = "akka.actor.default-dispatcher"
  697.  
  698. # Повністю кваліфікований шлях конфігурації, що містить конфігурацію
  699. # диспечера для акторів менеджмента селекторів
  700. management-dispatcher = "akka.actor.default-dispatcher"
  701.  
  702. # Повністю кваліфікований шлях конфігурації, що містить конфігурацію
  703. # диспечера, на якому плануються завдання файлового IO
  704. file-io-dispatcher = "akka.actor.default-dispatcher"
  705.  
  706. # Максимальне число байт (або "unlimited"), що передаються одним пакунком
  707. # при використанні комнади `WriteFile`, що використовує `FileChannel.transferTo`
  708. # для пересилки файлів в TCP сокет. На деяких OS, як Linux `FileChannel.transferTo`
  709. # може надовго блокувати, коли мережева передача IO швидша, ніж файлове IO.
  710. # Зменьшення знаяення може покращити чесність, тоді як збільшення може
  711. # покращити пропускну спроможність.
  712. file-io-transferTo-limit = 512 KiB
  713.  
  714. # Число разів, скільки треба повторити виклик `finishConnect` після повідомлення
  715. # про OP_CONNECT. Повтори потрібні, якщоповідомлення OP_CONNECT не очікує, що
  716. # `finishConnect` буде успішним, що відбувається під Android.
  717. finish-connect-retries = 5
  718.  
  719. # Під Windows обрив з'єднання детектуються ненадійно, якщо тільки OP_READ не
  720. # зареєстроване на селекторі _після_ того, як з'єднання було скинуте. Цей
  721. # обхідний маневр дозволяє OP_CONNECT, що змушує обрив бути помітним на Windows.
  722. # Включення цієї опції на інших платформах, ніж Windows, призводить до
  723. # різноманітних збоїв та невизначеної поведінки.
  724. # Можливі значення цього ключа є on, off та auto, де auto буде включати
  725. # обхід, якщо автоматично визначається Windows.
  726. windows-connection-abort-workaround-enabled = off
  727. }
  728.  
  729. udp {
  730.  
  731. # Число селекторів, щоб скинути обслуговувані канали; кожний з
  732. # них буде використовувати один цикл select на selector-dispatcher.
  733. nr-of-selectors = 1
  734.  
  735. # Максимальне число відкритих каналів, підтримуваних цім UDP модулем.
  736. # Загалом UDP не потребує великого числа каналів, таким чином, рекомендовано
  737. # утримувати цей параметр малим.
  738. max-channels = 4096
  739.  
  740. # Цикл select може використовуватись в двох режимах:
  741. # - встановлення "infinite" буде обирати без таймаута, прогинаючи потіік
  742. # - встановлення додатний таймаута буде викликати обмежений select,
  743. # дозволяючи використовувати один потік між декількома селекторами
  744. # (в цьому випадку вам треба використовувати різні конфігурації для
  745. # selector-dispatcher, тобто "type=Dispatcher" з розміром 1)
  746. # - встановлення його в нуль означає опит, тобто виклики selectNow()
  747. select-timeout = infinite
  748.  
  749. # При намаганні присвоїти нове з'єднання селектору, але обраний селектор
  750. # досяг межі ємності, повторити вибір та присвоєння таке число разів
  751. # перед припиненням спроб
  752. selector-association-retries = 10
  753.  
  754. # Максимальне число датаграм, що читаються за один раз, більші значення
  755. # зменшують затримки, меньші значення збільшують чесність на
  756. # worker-dispatcher
  757. receive-throughput = 3
  758.  
  759. # Число байт на один прямий буфер в пулі, що використовується для читання
  760. # або запису мережевих даних з ядра.
  761. direct-buffer-size = 128 KiB
  762.  
  763. # Максимальне число прямих буферів, що утримуються в пулі прямих буферів
  764. # для повторного використання.
  765. direct-buffer-pool-limit = 1000
  766.  
  767. # Максимальне число байт, доставлених повідомленням `Received`. Перед
  768. # тим, як читати більше даних з мережевого з'єднання, актор з'єднання спробує
  769. # виконати якусь іншу роботу.
  770. received-message-size-limit = unlimited
  771.  
  772. # Дозволяє гарно налаштувати журналювання щодо того, що відбувається в реалізації.
  773. # Майте на увазі, що до журналу можуть потрапляти більше одного запису на
  774. # повідомлення, надіслане актору по tcp реалізації.
  775. trace-logging = off
  776.  
  777. # Повністю кваліфікований шлях конфігурації, що містить конфігурацію диспечера,
  778. # що буде використаний для викликів select() в селекторах
  779. selector-dispatcher = "akka.io.pinned-dispatcher"
  780.  
  781. # Повністю кваліфікований шлях конфігурації, що містить конфігурацію диспечера
  782. # для робочих акторів читання/запису
  783. worker-dispatcher = "akka.actor.default-dispatcher"
  784.  
  785. # Повністю кваліфікований шлях конфігурації, що містить конфігурацію диспечера
  786. # для акторів менеджмента селекторів
  787. management-dispatcher = "akka.actor.default-dispatcher"
  788. }
  789.  
  790. udp-connected {
  791.  
  792. # Число селекторів, щоб скинути обслуговувані канали; кожний з
  793. # них буде використовувати один цикл select на selector-dispatcher.
  794. nr-of-selectors = 1
  795.  
  796. # Максимальне число відкритих каналів, підтримуваних цім UDP модулем.
  797. # Загалом, UDP не потребує великого числа каналів, і, таким чином,
  798. # рекомендовано утримувати це налаштування малим.
  799. max-channels = 4096
  800.  
  801. # Цикл select може використовуватись в двох режимах:
  802. # - встановлення "infinite" буде обирати без таймауту, прогинаючи потік
  803. # - встановлення додатний таймаута буде викликати обмежений select,
  804. # дозволяючи використовувати один потік між декількома селекторами
  805. # (в цьому випадку вам треба використовувати різні конфігурації для
  806. # selector-dispatcher, тобто "type=Dispatcher" з розміром 1)
  807. # - встановлення його в нуль означає опит, тобто виклики selectNow()
  808. select-timeout = infinite
  809.  
  810. # При спробі призначити нове з'єднання селектору, та обраний селектор
  811. # вичерпав повну ємність, повторювати вибір селектора та присвоєння
  812. # вказане число раз, перед припиненням спроб
  813. selector-association-retries = 10
  814.  
  815. # Максимальне число датаграм, що читаються за один раз, вищі значення
  816. # зменшують затримки, меньші значення збільшують чесність на
  817. # worker-dispatcher
  818. receive-throughput = 3
  819.  
  820. # Число байт на прямий буфер в пулі, що використовуються для читання та
  821. # запису мережевих даних з ядра.
  822. direct-buffer-size = 128 KiB
  823.  
  824. # Максимальне число прямих буферів, що утримуються в пулі прямих буферів
  825. # для повторного використання.
  826. direct-buffer-pool-limit = 1000
  827.  
  828. # Максимальне число байтів, доставлених через повідомлення `Received`.
  829. # Перед тим, як читати додаткові байти з мережі, актор з'єднання спробує
  830. # виконати інші завдання.
  831. received-message-size-limit = unlimited
  832.  
  833. # Дозволяє гарно налаштувати журналювання щодо того, що відбувається зсеердини
  834. # реалізації. Майте на увазі, що до журналу може потрапити більше одного
  835. # запису на повідомленяя, надіслане актору по tcp реалізації.
  836. trace-logging = off
  837.  
  838. # Повністю кваліфікований шлях конфігурації, що містить конфігурацію диспечера,
  839. # що буде використаний для викликів select() в селекторах
  840. selector-dispatcher = "akka.io.pinned-dispatcher"
  841.  
  842. # Повністю кваліфікований шлях конфігурації, що містить конфігурацію диспечера
  843. # для робочих акторів читання/запису
  844. worker-dispatcher = "akka.actor.default-dispatcher"
  845.  
  846. # Повністю кваліфікований шлях конфігурації, що містить конфігурацію диспечера
  847. # для акторів менеджменту селекторів
  848. management-dispatcher = "akka.actor.default-dispatcher"
  849. }
  850.  
  851. dns {
  852. # Повністю кваліфікований шлях конфігурації, що містить конфігурацію диспечера
  853. # для акторів менеджменту та ресолвера for маршрутів.
  854. # Для справжньої конфігурації маршрутизаторів дивіться akka.actor.deployment./IO-DNS/*
  855. dispatcher = "akka.actor.default-dispatcher"
  856.  
  857. # Ім'я підконфігурації по шляху akka.io.dns, дивіться inet-address нижче
  858. resolver = "inet-address"
  859.  
  860. inet-address {
  861. # Має реалізувати akka.io.DnsProvider
  862. provider-object = "akka.io.InetAddressDnsProvider"
  863.  
  864. # Ці TTL встановлені по замовчанню для java 6
  865. positive-ttl = 30s
  866. negative-ttl = 10s
  867.  
  868. # Як часто очищати застарілі елементи кешу.
  869. # Зауважте, що цей інтервал не має нічого загального з TTL
  870. cache-cleanup-interval = 120s
  871. }
  872. }
  873. }
  874.  
  875.  
  876. }

akka-agent

  1. ####################################
  2. # Akka Agent Reference Config File #
  3. ####################################
  4.  
  5. # This is the reference config file that contains all the default settings.
  6. # Make your edits/overrides in your application.conf.
  7.  
  8. akka {
  9. agent {
  10.  
  11. # The dispatcher used for agent-send-off actor
  12. send-off-dispatcher {
  13. executor = thread-pool-executor
  14. type = PinnedDispatcher
  15. }
  16.  
  17. # The dispatcher used for agent-alter-off actor
  18. alter-off-dispatcher {
  19. executor = thread-pool-executor
  20. type = PinnedDispatcher
  21. }
  22. }
  23. }

akka-camel

  1. ####################################
  2. # Akka Camel Reference Config File #
  3. ####################################
  4.  
  5. # This is the reference config file that contains all the default settings.
  6. # Make your edits/overrides in your application.conf.
  7.  
  8. akka {
  9. camel {
  10. # FQCN of the ContextProvider to be used to create or locate a CamelContext
  11. # it must implement akka.camel.ContextProvider and have a no-arg constructor
  12. # the built-in default create a fresh DefaultCamelContext
  13. context-provider = akka.camel.DefaultContextProvider
  14.  
  15. # Whether JMX should be enabled or disabled for the Camel Context
  16. jmx = off
  17. # enable/disable streaming cache on the Camel Context
  18. streamingCache = on
  19. consumer {
  20. # Configured setting which determines whether one-way communications
  21. # between an endpoint and this consumer actor
  22. # should be auto-acknowledged or application-acknowledged.
  23. # This flag has only effect when exchange is in-only.
  24. auto-ack = on
  25.  
  26. # When endpoint is out-capable (can produce responses) reply-timeout is the
  27. # maximum time the endpoint can take to send the response before the message
  28. # exchange fails. This setting is used for out-capable, in-only,
  29. # manually acknowledged communication.
  30. reply-timeout = 1m
  31.  
  32. # The duration of time to await activation of an endpoint.
  33. activation-timeout = 10s
  34. }
  35.  
  36. #Scheme to FQCN mappings for CamelMessage body conversions
  37. conversions {
  38. "file" = "java.io.InputStream"
  39. }
  40. }
  41. }

akka-cluster

  1. ######################################
  2. # Akka Cluster Reference Config File #
  3. ######################################
  4.  
  5. # This is the reference config file that contains all the default settings.
  6. # Make your edits/overrides in your application.conf.
  7.  
  8. akka {
  9.  
  10. cluster {
  11. # Initial contact points of the cluster.
  12. # The nodes to join automatically at startup.
  13. # Comma separated full URIs defined by a string on the form of
  14. # "akka.tcp://system@hostname:port"
  15. # Leave as empty if the node is supposed to be joined manually.
  16. seed-nodes = []
  17.  
  18. # how long to wait for one of the seed nodes to reply to initial join request
  19. seed-node-timeout = 5s
  20.  
  21. # If a join request fails it will be retried after this period.
  22. # Disable join retry by specifying "off".
  23. retry-unsuccessful-join-after = 10s
  24.  
  25. # Should the 'leader' in the cluster be allowed to automatically mark
  26. # unreachable nodes as DOWN after a configured time of unreachability?
  27. # Using auto-down implies that two separate clusters will automatically be
  28. # formed in case of network partition.
  29. # Disable with "off" or specify a duration to enable auto-down.
  30. auto-down-unreachable-after = off
  31. # Time margin after which shards or singletons that belonged to a downed/removed
  32. # partition are created in surviving partition. The purpose of this margin is that
  33. # in case of a network partition the persistent actors in the non-surviving partitions
  34. # must be stopped before corresponding persistent actors are started somewhere else.
  35. # This is useful if you implement downing strategies that handle network partitions,
  36. # e.g. by keeping the larger side of the partition and shutting down the smaller side.
  37. # It will not add any extra safety for auto-down-unreachable-after, since that is not
  38. # handling network partitions.
  39. # Disable with "off" or specify a duration to enable.
  40. down-removal-margin = off
  41.  
  42. # By default, the leader will not move 'Joining' members to 'Up' during a network
  43. # split. This feature allows the leader to accept 'Joining' members to be 'WeaklyUp'
  44. # so they become part of the cluster even during a network split. The leader will
  45. # move 'WeaklyUp' members to 'Up' status once convergence has been reached. This
  46. # feature must be off if some members are running Akka 2.3.X.
  47. # WeaklyUp is an EXPERIMENTAL feature.
  48. allow-weakly-up-members = off
  49.  
  50. # The roles of this member. List of strings, e.g. roles = ["A", "B"].
  51. # The roles are part of the membership information and can be used by
  52. # routers or other services to distribute work to certain member types,
  53. # e.g. front-end and back-end nodes.
  54. roles = []
  55.  
  56. role {
  57. # Minimum required number of members of a certain role before the leader
  58. # changes member status of 'Joining' members to 'Up'. Typically used together
  59. # with 'Cluster.registerOnMemberUp' to defer some action, such as starting
  60. # actors, until the cluster has reached a certain size.
  61. # E.g. to require 2 nodes with role 'frontend' and 3 nodes with role 'backend':
  62. # frontend.min-nr-of-members = 2
  63. # backend.min-nr-of-members = 3
  64. #.min-nr-of-members = 1
  65. }
  66.  
  67. # Minimum required number of members before the leader changes member status
  68. # of 'Joining' members to 'Up'. Typically used together with
  69. # 'Cluster.registerOnMemberUp' to defer some action, such as starting actors,
  70. # until the cluster has reached a certain size.
  71. min-nr-of-members = 1
  72.  
  73. # Enable/disable info level logging of cluster events
  74. log-info = on
  75.  
  76. # Enable or disable JMX MBeans for management of the cluster
  77. jmx.enabled = on
  78.  
  79. # how long should the node wait before starting the periodic tasks
  80. # maintenance tasks?
  81. periodic-tasks-initial-delay = 1s
  82.  
  83. # how often should the node send out gossip information?
  84. gossip-interval = 1s
  85. # discard incoming gossip messages if not handled within this duration
  86. gossip-time-to-live = 2s
  87.  
  88. # how often should the leader perform maintenance tasks?
  89. leader-actions-interval = 1s
  90.  
  91. # how often should the node move nodes, marked as unreachable by the failure
  92. # detector, out of the membership ring?
  93. unreachable-nodes-reaper-interval = 1s
  94.  
  95. # How often the current internal stats should be published.
  96. # A value of 0s can be used to always publish the stats, when it happens.
  97. # Disable with "off".
  98. publish-stats-interval = off
  99.  
  100. # The id of the dispatcher to use for cluster actors. If not specified
  101. # default dispatcher is used.
  102. # If specified you need to define the settings of the actual dispatcher.
  103. use-dispatcher = ""
  104.  
  105. # Gossip to random node with newer or older state information, if any with
  106. # this probability. Otherwise Gossip to any random live node.
  107. # Probability value is between 0.0 and 1.0. 0.0 means never, 1.0 means always.
  108. gossip-different-view-probability = 0.8
  109. # Reduced the above probability when the number of nodes in the cluster
  110. # greater than this value.
  111. reduce-gossip-different-view-probability = 400
  112.  
  113. # Settings for the Phi accrual failure detector (http://www.jaist.ac.jp/~defago/files/pdf/IS_RR_2004_010.pdf
  114. # [Hayashibara et al]) used by the cluster subsystem to detect unreachable
  115. # members.
  116. # The default PhiAccrualFailureDetector will trigger if there are no heartbeats within
  117. # the duration heartbeat-interval + acceptable-heartbeat-pause + threshold_adjustment,
  118. # i.e. around 5.5 seconds with default settings.
  119. failure-detector {
  120.  
  121. # FQCN of the failure detector implementation.
  122. # It must implement akka.remote.FailureDetector and have
  123. # a public constructor with a com.typesafe.config.Config and
  124. # akka.actor.EventStream parameter.
  125. implementation-class = "akka.remote.PhiAccrualFailureDetector"
  126.  
  127. # How often keep-alive heartbeat messages should be sent to each connection.
  128. heartbeat-interval = 1 s
  129.  
  130. # Defines the failure detector threshold.
  131. # A low threshold is prone to generate many wrong suspicions but ensures
  132. # a quick detection in the event of a real crash. Conversely, a high
  133. # threshold generates fewer mistakes but needs more time to detect
  134. # actual crashes.
  135. threshold = 8.0
  136.  
  137. # Number of the samples of inter-heartbeat arrival times to adaptively
  138. # calculate the failure timeout for connections.
  139. max-sample-size = 1000
  140.  
  141. # Minimum standard deviation to use for the normal distribution in
  142. # AccrualFailureDetector. Too low standard deviation might result in
  143. # too much sensitivity for sudden, but normal, deviations in heartbeat
  144. # inter arrival times.
  145. min-std-deviation = 100 ms
  146.  
  147. # Number of potentially lost/delayed heartbeats that will be
  148. # accepted before considering it to be an anomaly.
  149. # This margin is important to be able to survive sudden, occasional,
  150. # pauses in heartbeat arrivals, due to for example garbage collect or
  151. # network drop.
  152. acceptable-heartbeat-pause = 3 s
  153.  
  154. # Number of member nodes that each member will send heartbeat messages to,
  155. # i.e. each node will be monitored by this number of other nodes.
  156. monitored-by-nr-of-members = 5
  157. # After the heartbeat request has been sent the first failure detection
  158. # will start after this period, even though no heartbeat message has
  159. # been received.
  160. expected-response-after = 1 s
  161.  
  162. }
  163.  
  164. metrics {
  165. # Enable or disable metrics collector for load-balancing nodes.
  166. enabled = on
  167.  
  168. # FQCN of the metrics collector implementation.
  169. # It must implement akka.cluster.MetricsCollector and
  170. # have public constructor with akka.actor.ActorSystem parameter.
  171. # The default SigarMetricsCollector uses JMX and Hyperic SIGAR, if SIGAR
  172. # is on the classpath, otherwise only JMX.
  173. collector-class = "akka.cluster.SigarMetricsCollector"
  174.  
  175. # How often metrics are sampled on a node.
  176. # Shorter interval will collect the metrics more often.
  177. collect-interval = 3s
  178.  
  179. # How often a node publishes metrics information.
  180. gossip-interval = 3s
  181.  
  182. # How quickly the exponential weighting of past data is decayed compared to
  183. # new data. Set lower to increase the bias toward newer values.
  184. # The relevance of each data sample is halved for every passing half-life
  185. # duration, i.e. after 4 times the half-life, a data sample’s relevance is
  186. # reduced to 6% of its original relevance. The initial relevance of a data
  187. # sample is given by 1 – 0.5 ^ (collect-interval / half-life).
  188. # See http://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
  189. moving-average-half-life = 12s
  190. }
  191.  
  192. # If the tick-duration of the default scheduler is longer than the
  193. # tick-duration configured here a dedicated scheduler will be used for
  194. # periodic tasks of the cluster, otherwise the default scheduler is used.
  195. # See akka.scheduler settings for more details.
  196. scheduler {
  197. tick-duration = 33ms
  198. ticks-per-wheel = 512
  199. }
  200.  
  201. }
  202.  
  203. # Default configuration for routers
  204. actor.deployment.default {
  205. # MetricsSelector to use
  206. # - available: "mix", "heap", "cpu", "load"
  207. # - or: Fully qualified class name of the MetricsSelector class.
  208. # The class must extend akka.cluster.routing.MetricsSelector
  209. # and have a public constructor with com.typesafe.config.Config
  210. # parameter.
  211. # - default is "mix"
  212. metrics-selector = mix
  213. }
  214. actor.deployment.default.cluster {
  215. # enable cluster aware router that deploys to nodes in the cluster
  216. enabled = off
  217.  
  218. # Maximum number of routees that will be deployed on each cluster
  219. # member node.
  220. # Note that max-total-nr-of-instances defines total number of routees, but
  221. # number of routees per node will not be exceeded, i.e. if you
  222. # define max-total-nr-of-instances = 50 and max-nr-of-instances-per-node = 2
  223. # it will deploy 2 routees per new member in the cluster, up to
  224. # 25 members.
  225. max-nr-of-instances-per-node = 1
  226. # Maximum number of routees that will be deployed, in total
  227. # on all nodes. See also description of max-nr-of-instances-per-node.
  228. # For backwards compatibility reasons, nr-of-instances
  229. # has the same purpose as max-total-nr-of-instances for cluster
  230. # aware routers and nr-of-instances (if defined by user) takes
  231. # precedence over max-total-nr-of-instances.
  232. max-total-nr-of-instances = 10000
  233.  
  234. # Defines if routees are allowed to be located on the same node as
  235. # the head router actor, or only on remote nodes.
  236. # Useful for master-worker scenario where all routees are remote.
  237. allow-local-routees = on
  238.  
  239. # Use members with specified role, or all members if undefined or empty.
  240. use-role = ""
  241.  
  242. }
  243.  
  244. # Protobuf serializer for cluster messages
  245. actor {
  246. serializers {
  247. akka-cluster = "akka.cluster.protobuf.ClusterMessageSerializer"
  248. }
  249.  
  250. serialization-bindings {
  251. "akka.cluster.ClusterMessage" = akka-cluster
  252. }
  253. serialization-identifiers {
  254. "akka.cluster.protobuf.ClusterMessageSerializer" = 5
  255. }
  256. router.type-mapping {
  257. adaptive-pool = "akka.cluster.routing.AdaptiveLoadBalancingPool"
  258. adaptive-group = "akka.cluster.routing.AdaptiveLoadBalancingGroup"
  259. }
  260. }
  261.  
  262. }

akka-multi-node-testkit

  1. #############################################
  2. # Akka Remote Testing Reference Config File #
  3. #############################################
  4.  
  5. # This is the reference config file that contains all the default settings.
  6. # Make your edits/overrides in your application.conf.
  7.  
  8. akka {
  9. testconductor {
  10.  
  11. # Timeout for joining a barrier: this is the maximum time any participants
  12. # waits for everybody else to join a named barrier.
  13. barrier-timeout = 30s
  14. # Timeout for interrogation of TestConductor’s Controller actor
  15. query-timeout = 5s
  16. # Threshold for packet size in time unit above which the failure injector will
  17. # split the packet and deliver in smaller portions; do not give value smaller
  18. # than HashedWheelTimer resolution (would not make sense)
  19. packet-split-threshold = 100ms
  20. # amount of time for the ClientFSM to wait for the connection to the conductor
  21. # to be successful
  22. connect-timeout = 20s
  23. # Number of connect attempts to be made to the conductor controller
  24. client-reconnects = 30
  25. # minimum time interval which is to be inserted between reconnect attempts
  26. reconnect-backoff = 1s
  27.  
  28. netty {
  29. # (I&O) Used to configure the number of I/O worker threads on server sockets
  30. server-socket-worker-pool {
  31. # Min number of threads to cap factor-based number to
  32. pool-size-min = 1
  33.  
  34. # The pool size factor is used to determine thread pool size
  35. # using the following formula: ceil(available processors * factor).
  36. # Resulting size is then bounded by the pool-size-min and
  37. # pool-size-max values.
  38. pool-size-factor = 1.0
  39.  
  40. # Max number of threads to cap factor-based number to
  41. pool-size-max = 2
  42. }
  43.  
  44. # (I&O) Used to configure the number of I/O worker threads on client sockets
  45. client-socket-worker-pool {
  46. # Min number of threads to cap factor-based number to
  47. pool-size-min = 1
  48.  
  49. # The pool size factor is used to determine thread pool size
  50. # using the following formula: ceil(available processors * factor).
  51. # Resulting size is then bounded by the pool-size-min and
  52. # pool-size-max values.
  53. pool-size-factor = 1.0
  54.  
  55. # Max number of threads to cap factor-based number to
  56. pool-size-max = 2
  57. }
  58. }
  59. }
  60. }

akka-persistence

  1. ###########################################################
  2. # Akka Persistence Extension Reference Configuration File #
  3. ###########################################################
  4.  
  5. # This is the reference config file that contains all the default settings.
  6. # Make your edits in your application.conf in order to override these settings.
  7.  
  8. # Directory of persistence journal and snapshot store plugins is available at the
  9. # Akka Community Projects page http://akka.io/community/
  10.  
  11. # Default persistence extension settings.
  12. akka.persistence {
  13. journal {
  14. # Absolute path to the journal plugin configuration entry used by
  15. # persistent actor or view by default.
  16. # Persistent actor or view can override `journalPluginId` method
  17. # in order to rely on a different journal plugin.
  18. plugin = ""
  19. }
  20. snapshot-store {
  21. # Absolute path to the snapshot plugin configuration entry used by
  22. # persistent actor or view by default.
  23. # Persistent actor or view can override `snapshotPluginId` method
  24. # in order to rely on a different snapshot plugin.
  25. # It is not mandatory to specify a snapshot store plugin.
  26. # If you don't use snapshots you don't have to configure it.
  27. # Note that Cluster Sharding is using snapshots, so if you
  28. # use Cluster Sharding you need to define a snapshot store plugin.
  29. plugin = ""
  30. }
  31. # used as default-snapshot store if no plugin configured
  32. # (see `akka.persistence.snapshot-store`)
  33. no-snapshot-store {
  34. class = "akka.persistence.snapshot.NoSnapshotStore"
  35. }
  36. # Default persistent view settings.
  37. view {
  38. # Automated incremental view update.
  39. auto-update = on
  40. # Interval between incremental updates.
  41. auto-update-interval = 5s
  42. # Maximum number of messages to replay per incremental view update.
  43. # Set to -1 for no upper limit.
  44. auto-update-replay-max = -1
  45. }
  46. # Default reliable delivery settings.
  47. at-least-once-delivery {
  48. # Interval between re-delivery attempts.
  49. redeliver-interval = 5s
  50. # Maximum number of unconfirmed messages that will be sent in one
  51. # re-delivery burst.
  52. redelivery-burst-limit = 10000
  53. # After this number of delivery attempts a
  54. # `ReliableRedelivery.UnconfirmedWarning`, message will be sent to the actor.
  55. warn-after-number-of-unconfirmed-attempts = 5
  56. # Maximum number of unconfirmed messages that an actor with
  57. # AtLeastOnceDelivery is allowed to hold in memory.
  58. max-unconfirmed-messages = 100000
  59. }
  60. # Default persistent extension thread pools.
  61. dispatchers {
  62. # Dispatcher used by every plugin which does not declare explicit
  63. # `plugin-dispatcher` field.
  64. default-plugin-dispatcher {
  65. type = PinnedDispatcher
  66. executor = "thread-pool-executor"
  67. }
  68. # Default dispatcher for message replay.
  69. default-replay-dispatcher {
  70. type = Dispatcher
  71. executor = "fork-join-executor"
  72. fork-join-executor {
  73. parallelism-min = 2
  74. parallelism-max = 8
  75. }
  76. }
  77. # Default dispatcher for streaming snapshot IO
  78. default-stream-dispatcher {
  79. type = Dispatcher
  80. executor = "fork-join-executor"
  81. fork-join-executor {
  82. parallelism-min = 2
  83. parallelism-max = 8
  84. }
  85. }
  86. }
  87. # Fallback settings for journal plugin configurations.
  88. # These settings are used if they are not defined in plugin config section.
  89. journal-plugin-fallback {
  90. # Fully qualified class name providing journal plugin api implementation.
  91. # It is mandatory to specify this property.
  92. # The class must have a constructor without parameters or constructor with
  93. # one `com.typesafe.config.Config` parameter.
  94. class = ""
  95.  
  96. # Dispatcher for the plugin actor.
  97. plugin-dispatcher = "akka.persistence.dispatchers.default-plugin-dispatcher"
  98. # Dispatcher for message replay.
  99. replay-dispatcher = "akka.persistence.dispatchers.default-replay-dispatcher"
  100. # Maximum size of a persistent message batch written to the journal.
  101. max-message-batch-size = 200
  102. circuit-breaker {
  103. max-failures = 10
  104. call-timeout = 10s
  105. reset-timeout = 30s
  106. }
  107. # The replay filter can detect a corrupt event stream by inspecting
  108. # sequence numbers and writerUuid when replaying events.
  109. replay-filter {
  110. # What the filter should do when detecting invalid events.
  111. # Supported values:
  112. # `repair-by-discard-old` : discard events from old writers,
  113. # warning is logged
  114. # `fail` : fail the replay, error is logged
  115. # `warn` : log warning but emit events untouched
  116. # `off` : disable this feature completely
  117. mode = repair-by-discard-old
  118. # It uses a look ahead buffer for analyzing the events.
  119. # This defines the size (in number of events) of the buffer.
  120. window-size = 100
  121. # How many old writerUuid to remember
  122. max-old-writers = 10
  123. }
  124. }
  125.  
  126. # Fallback settings for snapshot store plugin configurations
  127. # These settings are used if they are not defined in plugin config section.
  128. snapshot-store-plugin-fallback {
  129. # Fully qualified class name providing snapshot store plugin api
  130. # implementation. It is mandatory to specify this property if
  131. # snapshot store is enabled.
  132. # The class must have a constructor without parameters or constructor with
  133. # one `com.typesafe.config.Config` parameter.
  134. class = ""
  135.  
  136. # Dispatcher for the plugin actor.
  137. plugin-dispatcher = "akka.persistence.dispatchers.default-plugin-dispatcher"
  138. circuit-breaker {
  139. max-failures = 5
  140. call-timeout = 20s
  141. reset-timeout = 60s
  142. }
  143. }
  144. }
  145.  
  146. # Protobuf serialization for the persistent extension messages.
  147. akka.actor {
  148. serializers {
  149. akka-persistence-message = "akka.persistence.serialization.MessageSerializer"
  150. akka-persistence-snapshot = "akka.persistence.serialization.SnapshotSerializer"
  151. }
  152. serialization-bindings {
  153. "akka.persistence.serialization.Message" = akka-persistence-message
  154. "akka.persistence.serialization.Snapshot" = akka-persistence-snapshot
  155. }
  156. serialization-identifiers {
  157. "akka.persistence.serialization.MessageSerializer" = 7
  158. "akka.persistence.serialization.SnapshotSerializer" = 8
  159. }
  160. }
  161.  
  162.  
  163. ###################################################
  164. # Persistence plugins included with the extension #
  165. ###################################################
  166.  
  167. # In-memory journal plugin.
  168. akka.persistence.journal.inmem {
  169. # Class name of the plugin.
  170. class = "akka.persistence.journal.inmem.InmemJournal"
  171. # Dispatcher for the plugin actor.
  172. plugin-dispatcher = "akka.actor.default-dispatcher"
  173. }
  174.  
  175. # Local file system snapshot store plugin.
  176. akka.persistence.snapshot-store.local {
  177. # Class name of the plugin.
  178. class = "akka.persistence.snapshot.local.LocalSnapshotStore"
  179. # Dispatcher for the plugin actor.
  180. plugin-dispatcher = "akka.persistence.dispatchers.default-plugin-dispatcher"
  181. # Dispatcher for streaming snapshot IO.
  182. stream-dispatcher = "akka.persistence.dispatchers.default-stream-dispatcher"
  183. # Storage location of snapshot files.
  184. dir = "snapshots"
  185. # Number load attempts when recovering from the latest snapshot fails
  186. # yet older snapshot files are available. Each recovery attempt will try
  187. # to recover using an older than previously failed-on snapshot file
  188. # (if any are present).
  189. max-load-attempts = 3
  190. }
  191.  
  192. # LevelDB journal plugin.
  193. # Note: this plugin requires explicit LevelDB dependency, see below.
  194. akka.persistence.journal.leveldb {
  195. # Class name of the plugin.
  196. class = "akka.persistence.journal.leveldb.LeveldbJournal"
  197. # Dispatcher for the plugin actor.
  198. plugin-dispatcher = "akka.persistence.dispatchers.default-plugin-dispatcher"
  199. # Dispatcher for message replay.
  200. replay-dispatcher = "akka.persistence.dispatchers.default-replay-dispatcher"
  201. # Storage location of LevelDB files.
  202. dir = "journal"
  203. # Use fsync on write.
  204. fsync = on
  205. # Verify checksum on read.
  206. checksum = off
  207. # Native LevelDB (via JNI) or LevelDB Java port.
  208. native = on
  209. }
  210.  
  211. # Shared LevelDB journal plugin (for testing only).
  212. # Note: this plugin requires explicit LevelDB dependency, see below.
  213. akka.persistence.journal.leveldb-shared {
  214. # Class name of the plugin.
  215. class = "akka.persistence.journal.leveldb.SharedLeveldbJournal"
  216. # Dispatcher for the plugin actor.
  217. plugin-dispatcher = "akka.actor.default-dispatcher"
  218. # Timeout for async journal operations.
  219. timeout = 10s
  220. store {
  221. # Dispatcher for shared store actor.
  222. store-dispatcher = "akka.persistence.dispatchers.default-plugin-dispatcher"
  223. # Dispatcher for message replay.
  224. replay-dispatcher = "akka.persistence.dispatchers.default-replay-dispatcher"
  225. # Storage location of LevelDB files.
  226. dir = "journal"
  227. # Use fsync on write.
  228. fsync = on
  229. # Verify checksum on read.
  230. checksum = off
  231. # Native LevelDB (via JNI) or LevelDB Java port.
  232. native = on
  233. }
  234. }
  235.  
  236. # LevelDB persistence requires the following dependency declarations:
  237. #
  238. # SBT:
  239. # "org.iq80.leveldb" % "leveldb" % "0.7"
  240. # "org.fusesource.leveldbjni" % "leveldbjni-all" % "1.8"
  241. #
  242. # Maven:
  243. #
  244. # org.iq80.leveldb
  245. # leveldb
  246. # 0.7
  247. #
  248. #
  249. # org.fusesource.leveldbjni
  250. # leveldbjni-all
  251. # 1.8
  252. #

akka-remote

  1. #####################################
  2. # Akka Remote Reference Config File #
  3. #####################################
  4.  
  5. # This is the reference config file that contains all the default settings.
  6. # Make your edits/overrides in your application.conf.
  7.  
  8. # comments about akka.actor settings left out where they are already in akka-
  9. # actor.jar, because otherwise they would be repeated in config rendering.
  10.  
  11. akka {
  12.  
  13. actor {
  14.  
  15. serializers {
  16. akka-containers = "akka.remote.serialization.MessageContainerSerializer"
  17. proto = "akka.remote.serialization.ProtobufSerializer"
  18. daemon-create = "akka.remote.serialization.DaemonMsgCreateSerializer"
  19. }
  20.  
  21. serialization-bindings {
  22. "akka.actor.ActorSelectionMessage" = akka-containers
  23. "akka.remote.DaemonMsgCreate" = daemon-create
  24. # Since akka.protobuf.Message does not extend Serializable but
  25. # GeneratedMessage does, need to use the more specific one here in order
  26. # to avoid ambiguity.
  27. "akka.protobuf.GeneratedMessage" = proto
  28. # Since com.google.protobuf.Message does not extend Serializable but
  29. # GeneratedMessage does, need to use the more specific one here in order
  30. # to avoid ambiguity.
  31. # This com.google.protobuf serialization binding is only used if the class can be loaded,
  32. # i.e. com.google.protobuf dependency has been added in the application project.
  33. "com.google.protobuf.GeneratedMessage" = proto
  34. }
  35.  
  36. serialization-identifiers {
  37. "akka.remote.serialization.ProtobufSerializer" = 2
  38. "akka.remote.serialization.DaemonMsgCreateSerializer" = 3
  39. "akka.remote.serialization.MessageContainerSerializer" = 6
  40. }
  41.  
  42. deployment {
  43.  
  44. default {
  45.  
  46. # if this is set to a valid remote address, the named actor will be
  47. # deployed at that node e.g. "akka.tcp://sys@host:port"
  48. remote = ""
  49.  
  50. target {
  51.  
  52. # A list of hostnames and ports for instantiating the children of a
  53. # router
  54. # The format should be on "akka.tcp://sys@host:port", where:
  55. # - sys is the remote actor system name
  56. # - hostname can be either hostname or IP address the remote actor
  57. # should connect to
  58. # - port should be the port for the remote server on the other node
  59. # The number of actor instances to be spawned is still taken from the
  60. # nr-of-instances setting as for local routers; the instances will be
  61. # distributed round-robin among the given nodes.
  62. nodes = []
  63.  
  64. }
  65. }
  66. }
  67. }
  68.  
  69. remote {
  70.  
  71. ### General settings
  72.  
  73. # Timeout after which the startup of the remoting subsystem is considered
  74. # to be failed. Increase this value if your transport drivers (see the
  75. # enabled-transports section) need longer time to be loaded.
  76. startup-timeout = 10 s
  77.  
  78. # Timout after which the graceful shutdown of the remoting subsystem is
  79. # considered to be failed. After the timeout the remoting system is
  80. # forcefully shut down. Increase this value if your transport drivers
  81. # (see the enabled-transports section) need longer time to stop properly.
  82. shutdown-timeout = 10 s
  83.  
  84. # Before shutting down the drivers, the remoting subsystem attempts to flush
  85. # all pending writes. This setting controls the maximum time the remoting is
  86. # willing to wait before moving on to shut down the drivers.
  87. flush-wait-on-shutdown = 2 s
  88.  
  89. # Reuse inbound connections for outbound messages
  90. use-passive-connections = on
  91.  
  92. # Controls the backoff interval after a refused write is reattempted.
  93. # (Transports may refuse writes if their internal buffer is full)
  94. backoff-interval = 5 ms
  95.  
  96. # Acknowledgment timeout of management commands sent to the transport stack.
  97. command-ack-timeout = 30 s
  98. # The timeout for outbound associations to perform the handshake.
  99. # If the transport is akka.remote.netty.tcp or akka.remote.netty.ssl
  100. # the configured connection-timeout for the transport will be used instead.
  101. handshake-timeout = 15 s
  102.  
  103. # If set to a nonempty string remoting will use the given dispatcher for
  104. # its internal actors otherwise the default dispatcher is used. Please note
  105. # that since remoting can load arbitrary 3rd party drivers (see
  106. # "enabled-transport" and "adapters" entries) it is not guaranteed that
  107. # every module will respect this setting.
  108. use-dispatcher = "akka.remote.default-remote-dispatcher"
  109.  
  110. ### Security settings
  111.  
  112. # Enable untrusted mode for full security of server managed actors, prevents
  113. # system messages to be send by clients, e.g. messages like 'Create',
  114. # 'Suspend', 'Resume', 'Terminate', 'Supervise', 'Link' etc.
  115. untrusted-mode = off
  116. # When 'untrusted-mode=on' inbound actor selections are by default discarded.
  117. # Actors with paths defined in this white list are granted permission to receive actor
  118. # selections messages.
  119. # E.g. trusted-selection-paths = ["/user/receptionist", "/user/namingService"]
  120. trusted-selection-paths = []
  121.  
  122. # Should the remote server require that its peers share the same
  123. # secure-cookie (defined in the 'remote' section)? Secure cookies are passed
  124. # between during the initial handshake. Connections are refused if the initial
  125. # message contains a mismatching cookie or the cookie is missing.
  126. require-cookie = off
  127.  
  128. # Deprecated since 2.4-M1
  129. secure-cookie = ""
  130.  
  131. ### Logging
  132.  
  133. # If this is "on", Akka will log all inbound messages at DEBUG level,
  134. # if off then they are not logged
  135. log-received-messages = off
  136.  
  137. # If this is "on", Akka will log all outbound messages at DEBUG level,
  138. # if off then they are not logged
  139. log-sent-messages = off
  140.  
  141. # Sets the log granularity level at which Akka logs remoting events. This setting
  142. # can take the values OFF, ERROR, WARNING, INFO, DEBUG, or ON. For compatibility
  143. # reasons the setting "on" will default to "debug" level. Please note that the effective
  144. # logging level is still determined by the global logging level of the actor system:
  145. # for example debug level remoting events will be only logged if the system
  146. # is running with debug level logging.
  147. # Failures to deserialize received messages also fall under this flag.
  148. log-remote-lifecycle-events = on
  149.  
  150. # Logging of message types with payload size in bytes larger than
  151. # this value. Maximum detected size per message type is logged once,
  152. # with an increase threshold of 10%.
  153. # By default this feature is turned off. Activate it by setting the property to
  154. # a value in bytes, such as 1000b. Note that for all messages larger than this
  155. # limit there will be extra performance and scalability cost.
  156. log-frame-size-exceeding = off
  157. # Log warning if the number of messages in the backoff buffer in the endpoint
  158. # writer exceeds this limit. It can be disabled by setting the value to off.
  159. log-buffer-size-exceeding = 50000
  160.  
  161. ### Failure detection and recovery
  162.  
  163. # Settings for the failure detector to monitor connections.
  164. # For TCP it is not important to have fast failure detection, since
  165. # most connection failures are captured by TCP itself.
  166. # The default DeadlineFailureDetector will trigger if there are no heartbeats within
  167. # the duration heartbeat-interval + acceptable-heartbeat-pause, i.e. 20 seconds
  168. # with the default settings.
  169. transport-failure-detector {
  170.  
  171. # FQCN of the failure detector implementation.
  172. # It must implement akka.remote.FailureDetector and have
  173. # a public constructor with a com.typesafe.config.Config and
  174. # akka.actor.EventStream parameter.
  175. implementation-class = "akka.remote.DeadlineFailureDetector"
  176.  
  177. # How often keep-alive heartbeat messages should be sent to each connection.
  178. heartbeat-interval = 4 s
  179.  
  180. # Number of potentially lost/delayed heartbeats that will be
  181. # accepted before considering it to be an anomaly.
  182. # A margin to the `heartbeat-interval` is important to be able to survive sudden,
  183. # occasional, pauses in heartbeat arrivals, due to for example garbage collect or
  184. # network drop.
  185. acceptable-heartbeat-pause = 16 s
  186. }
  187.  
  188. # Settings for the Phi accrual failure detector (http://www.jaist.ac.jp/~defago/files/pdf/IS_RR_2004_010.pdf
  189. # [Hayashibara et al]) used for remote death watch.
  190. # The default PhiAccrualFailureDetector will trigger if there are no heartbeats within
  191. # the duration heartbeat-interval + acceptable-heartbeat-pause + threshold_adjustment,
  192. # i.e. around 12.5 seconds with default settings.
  193. watch-failure-detector {
  194.  
  195. # FQCN of the failure detector implementation.
  196. # It must implement akka.remote.FailureDetector and have
  197. # a public constructor with a com.typesafe.config.Config and
  198. # akka.actor.EventStream parameter.
  199. implementation-class = "akka.remote.PhiAccrualFailureDetector"
  200.  
  201. # How often keep-alive heartbeat messages should be sent to each connection.
  202. heartbeat-interval = 1 s
  203.  
  204. # Defines the failure detector threshold.
  205. # A low threshold is prone to generate many wrong suspicions but ensures
  206. # a quick detection in the event of a real crash. Conversely, a high
  207. # threshold generates fewer mistakes but needs more time to detect
  208. # actual crashes.
  209. threshold = 10.0
  210.  
  211. # Number of the samples of inter-heartbeat arrival times to adaptively
  212. # calculate the failure timeout for connections.
  213. max-sample-size = 200
  214.  
  215. # Minimum standard deviation to use for the normal distribution in
  216. # AccrualFailureDetector. Too low standard deviation might result in
  217. # too much sensitivity for sudden, but normal, deviations in heartbeat
  218. # inter arrival times.
  219. min-std-deviation = 100 ms
  220.  
  221. # Number of potentially lost/delayed heartbeats that will be
  222. # accepted before considering it to be an anomaly.
  223. # This margin is important to be able to survive sudden, occasional,
  224. # pauses in heartbeat arrivals, due to for example garbage collect or
  225. # network drop.
  226. acceptable-heartbeat-pause = 10 s
  227.  
  228.  
  229. # How often to check for nodes marked as unreachable by the failure
  230. # detector
  231. unreachable-nodes-reaper-interval = 1s
  232.  
  233. # After the heartbeat request has been sent the first failure detection
  234. # will start after this period, even though no heartbeat mesage has
  235. # been received.
  236. expected-response-after = 1 s
  237.  
  238. }
  239.  
  240. # After failed to establish an outbound connection, the remoting will mark the
  241. # address as failed. This configuration option controls how much time should
  242. # be elapsed before reattempting a new connection. While the address is
  243. # gated, all messages sent to the address are delivered to dead-letters.
  244. # Since this setting limits the rate of reconnects setting it to a
  245. # very short interval (i.e. less than a second) may result in a storm of
  246. # reconnect attempts.
  247. retry-gate-closed-for = 5 s
  248.  
  249. # After catastrophic communication failures that result in the loss of system
  250. # messages or after the remote DeathWatch triggers the remote system gets
  251. # quarantined to prevent inconsistent behavior.
  252. # This setting controls how long the Quarantine marker will be kept around
  253. # before being removed to avoid long-term memory leaks.
  254. # WARNING: DO NOT change this to a small value to re-enable communication with
  255. # quarantined nodes. Such feature is not supported and any behavior between
  256. # the affected systems after lifting the quarantine is undefined.
  257. prune-quarantine-marker-after = 5 d
  258.  
  259. # If system messages have been exchanged between two systems (i.e. remote death
  260. # watch or remote deployment has been used) a remote system will be marked as
  261. # quarantined after the two system has no active association, and no
  262. # communication happens during the time configured here.
  263. # The only purpose of this setting is to avoid storing system message redelivery
  264. # data (sequence number state, etc.) for an undefined amount of time leading to long
  265. # term memory leak. Instead, if a system has been gone for this period,
  266. # or more exactly
  267. # - there is no association between the two systems (TCP connection, if TCP transport is used)
  268. # - neither side has been attempting to communicate with the other
  269. # - there are no pending system messages to deliver
  270. # for the amount of time configured here, the remote system will be quarantined and all state
  271. # associated with it will be dropped.
  272. quarantine-after-silence = 5 d
  273.  
  274. # This setting defines the maximum number of unacknowledged system messages
  275. # allowed for a remote system. If this limit is reached the remote system is
  276. # declared to be dead and its UID marked as tainted.
  277. system-message-buffer-size = 20000
  278.  
  279. # This setting defines the maximum idle time after an individual
  280. # acknowledgement for system messages is sent. System message delivery
  281. # is guaranteed by explicit acknowledgement messages. These acks are
  282. # piggybacked on ordinary traffic messages. If no traffic is detected
  283. # during the time period configured here, the remoting will send out
  284. # an individual ack.
  285. system-message-ack-piggyback-timeout = 0.3 s
  286.  
  287. # This setting defines the time after internal management signals
  288. # between actors (used for DeathWatch and supervision) that have not been
  289. # explicitly acknowledged or negatively acknowledged are resent.
  290. # Messages that were negatively acknowledged are always immediately
  291. # resent.
  292. resend-interval = 2 s
  293. # Maximum number of unacknowledged system messages that will be resent
  294. # each 'resend-interval'. If you watch many (> 1000) remote actors you can
  295. # increase this value to for example 600, but a too large limit (e.g. 10000)
  296. # may flood the connection and might cause false failure detection to trigger.
  297. # Test such a configuration by watching all actors at the same time and stop
  298. # all watched actors at the same time.
  299. resend-limit = 200
  300.  
  301. # WARNING: this setting should not be not changed unless all of its consequences
  302. # are properly understood which assumes experience with remoting internals
  303. # or expert advice.
  304. # This setting defines the time after redelivery attempts of internal management
  305. # signals are stopped to a remote system that has been not confirmed to be alive by
  306. # this system before.
  307. initial-system-message-delivery-timeout = 3 m
  308.  
  309. ### Transports and adapters
  310.  
  311. # List of the transport drivers that will be loaded by the remoting.
  312. # A list of fully qualified config paths must be provided where
  313. # the given configuration path contains a transport-class key
  314. # pointing to an implementation class of the Transport interface.
  315. # If multiple transports are provided, the address of the first
  316. # one will be used as a default address.
  317. enabled-transports = ["akka.remote.netty.tcp"]
  318.  
  319. # Transport drivers can be augmented with adapters by adding their
  320. # name to the applied-adapters setting in the configuration of a
  321. # transport. The available adapters should be configured in this
  322. # section by providing a name, and the fully qualified name of
  323. # their corresponding implementation. The class given here
  324. # must implement akka.akka.remote.transport.TransportAdapterProvider
  325. # and have public constructor without parameters.
  326. adapters {
  327. gremlin = "akka.remote.transport.FailureInjectorProvider"
  328. trttl = "akka.remote.transport.ThrottlerProvider"
  329. }
  330.  
  331. ### Default configuration for the Netty based transport drivers
  332.  
  333. netty.tcp {
  334. # The class given here must implement the akka.remote.transport.Transport
  335. # interface and offer a public constructor which takes two arguments:
  336. # 1) akka.actor.ExtendedActorSystem
  337. # 2) com.typesafe.config.Config
  338. transport-class = "akka.remote.transport.netty.NettyTransport"
  339.  
  340. # Transport drivers can be augmented with adapters by adding their
  341. # name to the applied-adapters list. The last adapter in the
  342. # list is the adapter immediately above the driver, while
  343. # the first one is the top of the stack below the standard
  344. # Akka protocol
  345. applied-adapters = []
  346.  
  347. transport-protocol = tcp
  348.  
  349. # The default remote server port clients should connect to.
  350. # Default is 2552 (AKKA), use 0 if you want a random available port
  351. # This port needs to be unique for each actor system on the same machine.
  352. port = 2552
  353.  
  354. # The hostname or ip clients should connect to.
  355. # InetAddress.getLocalHost.getHostAddress is used if empty
  356. hostname = ""
  357.  
  358. # Use this setting to bind a network interface to a different port
  359. # than remoting protocol expects messages at. This may be used
  360. # when running akka nodes in a separated networks (under NATs or docker containers).
  361. # Use 0 if you want a random available port. Examples:
  362. #
  363. # akka.remote.netty.tcp.port = 2552
  364. # akka.remote.netty.tcp.bind-port = 2553
  365. # Network interface will be bound to the 2553 port, but remoting protocol will
  366. # expect messages sent to port 2552.
  367. #
  368. # akka.remote.netty.tcp.port = 0
  369. # akka.remote.netty.tcp.bind-port = 0
  370. # Network interface will be bound to a random port, and remoting protocol will
  371. # expect messages sent to the bound port.
  372. #
  373. # akka.remote.netty.tcp.port = 2552
  374. # akka.remote.netty.tcp.bind-port = 0
  375. # Network interface will be bound to a random port, but remoting protocol will
  376. # expect messages sent to port 2552.
  377. #
  378. # akka.remote.netty.tcp.port = 0
  379. # akka.remote.netty.tcp.bind-port = 2553
  380. # Network interface will be bound to the 2553 port, and remoting protocol will
  381. # expect messages sent to the bound port.
  382. #
  383. # akka.remote.netty.tcp.port = 2552
  384. # akka.remote.netty.tcp.bind-port = ""
  385. # Network interface will be bound to the 2552 port, and remoting protocol will
  386. # expect messages sent to the bound port.
  387. #
  388. # akka.remote.netty.tcp.port if empty
  389. bind-port = ""
  390.  
  391. # Use this setting to bind a network interface to a different hostname or ip
  392. # than remoting protocol expects messages at.
  393. # Use "0.0.0.0" to bind to all interfaces.
  394. # akka.remote.netty.tcp.hostname if empty
  395. bind-hostname = ""
  396.  
  397. # Enables SSL support on this transport
  398. enable-ssl = false
  399.  
  400. # Sets the connectTimeoutMillis of all outbound connections,
  401. # i.e. how long a connect may take until it is timed out
  402. connection-timeout = 15 s
  403.  
  404. # If set to "" then the specified dispatcher
  405. # will be used to accept inbound connections, and perform IO. If "" then
  406. # dedicated threads will be used.
  407. # Please note that the Netty driver only uses this configuration and does
  408. # not read the "akka.remote.use-dispatcher" entry. Instead it has to be
  409. # configured manually to point to the same dispatcher if needed.
  410. use-dispatcher-for-io = ""
  411.  
  412. # Sets the high water mark for the in and outbound sockets,
  413. # set to 0b for platform default
  414. write-buffer-high-water-mark = 0b
  415.  
  416. # Sets the low water mark for the in and outbound sockets,
  417. # set to 0b for platform default
  418. write-buffer-low-water-mark = 0b
  419.  
  420. # Sets the send buffer size of the Sockets,
  421. # set to 0b for platform default
  422. send-buffer-size = 256000b
  423.  
  424. # Sets the receive buffer size of the Sockets,
  425. # set to 0b for platform default
  426. receive-buffer-size = 256000b
  427.  
  428. # Maximum message size the transport will accept, but at least
  429. # 32000 bytes.
  430. # Please note that UDP does not support arbitrary large datagrams,
  431. # so this setting has to be chosen carefully when using UDP.
  432. # Both send-buffer-size and receive-buffer-size settings has to
  433. # be adjusted to be able to buffer messages of maximum size.
  434. maximum-frame-size = 128000b
  435.  
  436. # Sets the size of the connection backlog
  437. backlog = 4096
  438.  
  439. # Enables the TCP_NODELAY flag, i.e. disables Nagle’s algorithm
  440. tcp-nodelay = on
  441.  
  442. # Enables TCP Keepalive, subject to the O/S kernel’s configuration
  443. tcp-keepalive = on
  444.  
  445. # Enables SO_REUSEADDR, which determines when an ActorSystem can open
  446. # the specified listen port (the meaning differs between *nix and Windows)
  447. # Valid values are "on", "off" and "off-for-windows"
  448. # due to the following Windows bug: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4476378
  449. # "off-for-windows" of course means that it's "on" for all other platforms
  450. tcp-reuse-addr = off-for-windows
  451.  
  452. # Used to configure the number of I/O worker threads on server sockets
  453. server-socket-worker-pool {
  454. # Min number of threads to cap factor-based number to
  455. pool-size-min = 2
  456.  
  457. # The pool size factor is used to determine thread pool size
  458. # using the following formula: ceil(available processors * factor).
  459. # Resulting size is then bounded by the pool-size-min and
  460. # pool-size-max values.
  461. pool-size-factor = 1.0
  462.  
  463. # Max number of threads to cap factor-based number to
  464. pool-size-max = 2
  465. }
  466.  
  467. # Used to configure the number of I/O worker threads on client sockets
  468. client-socket-worker-pool {
  469. # Min number of threads to cap factor-based number to
  470. pool-size-min = 2
  471.  
  472. # The pool size factor is used to determine thread pool size
  473. # using the following formula: ceil(available processors * factor).
  474. # Resulting size is then bounded by the pool-size-min and
  475. # pool-size-max values.
  476. pool-size-factor = 1.0
  477.  
  478. # Max number of threads to cap factor-based number to
  479. pool-size-max = 2
  480. }
  481.  
  482.  
  483. }
  484.  
  485. netty.udp = ${akka.remote.netty.tcp}
  486. netty.udp {
  487. transport-protocol = udp
  488. }
  489.  
  490. netty.ssl = ${akka.remote.netty.tcp}
  491. netty.ssl = {
  492. # Enable SSL/TLS encryption.
  493. # This must be enabled on both the client and server to work.
  494. enable-ssl = true
  495.  
  496. security {
  497. # This is the Java Key Store used by the server connection
  498. key-store = "keystore"
  499.  
  500. # This password is used for decrypting the key store
  501. key-store-password = "changeme"
  502.  
  503. # This password is used for decrypting the key
  504. key-password = "changeme"
  505.  
  506. # This is the Java Key Store used by the client connection
  507. trust-store = "truststore"
  508.  
  509. # This password is used for decrypting the trust store
  510. trust-store-password = "changeme"
  511.  
  512. # Protocol to use for SSL encryption, choose from:
  513. # Java 6 & 7:
  514. # 'SSLv3', 'TLSv1'
  515. # Java 7:
  516. # 'TLSv1.1', 'TLSv1.2'
  517. protocol = "TLSv1"
  518.  
  519. # Example: ["TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_256_CBC_SHA"]
  520. # You need to install the JCE Unlimited Strength Jurisdiction Policy
  521. # Files to use AES 256.
  522. # More info here:
  523. # http://docs.oracle.com/javase/7/docs/technotes/guides/security/SunProviders.html#SunJCEProvider
  524. enabled-algorithms = ["TLS_RSA_WITH_AES_128_CBC_SHA"]
  525.  
  526. # There are three options, in increasing order of security:
  527. # "" or SecureRandom => (default)
  528. # "SHA1PRNG" => Can be slow because of blocking issues on Linux
  529. # "AES128CounterSecureRNG" => fastest startup and based on AES encryption
  530. # algorithm
  531. # "AES256CounterSecureRNG"
  532. #
  533. # The following are deprecated in Akka 2.4. They use one of 3 possible
  534. # seed sources, depending on availability: /dev/random, random.org and
  535. # SecureRandom (provided by Java)
  536. # "AES128CounterInetRNG"
  537. # "AES256CounterInetRNG" (Install JCE Unlimited Strength Jurisdiction
  538. # Policy Files first)
  539. # Setting a value here may require you to supply the appropriate cipher
  540. # suite (see enabled-algorithms section above)
  541. random-number-generator = ""
  542. }
  543. }
  544.  
  545. ### Default configuration for the failure injector transport adapter
  546.  
  547. gremlin {
  548. # Enable debug logging of the failure injector transport adapter
  549. debug = off
  550. }
  551.  
  552. ### Default dispatcher for the remoting subsystem
  553.  
  554. default-remote-dispatcher {
  555. type = Dispatcher
  556. executor = "fork-join-executor"
  557. fork-join-executor {
  558. # Min number of threads to cap factor-based parallelism number to
  559. parallelism-min = 2
  560. parallelism-max = 2
  561. }
  562. }
  563. backoff-remote-dispatcher {
  564. type = Dispatcher
  565. executor = "fork-join-executor"
  566. fork-join-executor {
  567. # Min number of threads to cap factor-based parallelism number to
  568. parallelism-min = 2
  569. parallelism-max = 2
  570. }
  571. }
  572.  
  573.  
  574. }
  575.  
  576. }

akka-testkit

  1. ######################################
  2. # Akka Testkit Reference Config File #
  3. ######################################
  4.  
  5. # This is the reference config file that contains all the default settings.
  6. # Make your edits/overrides in your application.conf.
  7.  
  8. akka {
  9. test {
  10. # factor by which to scale timeouts during tests, e.g. to account for shared
  11. # build system load
  12. timefactor = 1.0
  13.  
  14. # duration of EventFilter.intercept waits after the block is finished until
  15. # all required messages are received
  16. filter-leeway = 3s
  17.  
  18. # duration to wait in expectMsg and friends outside of within() block
  19. # by default
  20. single-expect-default = 3s
  21.  
  22. # The timeout that is added as an implicit by DefaultTimeout trait
  23. default-timeout = 5s
  24.  
  25. calling-thread-dispatcher {
  26. type = akka.testkit.CallingThreadDispatcherConfigurator
  27. }
  28. }
  29. }

akka-cluster-metrics ~~~~~~~~~~~~--------

  1. ##############################################
  2. # Akka Cluster Metrics Reference Config File #
  3. ##############################################
  4.  
  5. # This is the reference config file that contains all the default settings.
  6. # Make your edits in your application.conf in order to override these settings.
  7.  
  8. # Sigar provisioning:
  9. #
  10. # User can provision sigar classes and native library in one of the following ways:
  11. #
  12. # 1) Use https://github.com/kamon-io/sigar-loader Kamon sigar-loader as a project dependency for the user project.
  13. # Metrics extension will extract and load sigar library on demand with help of Kamon sigar provisioner.
  14. #
  15. # 2) Use https://github.com/kamon-io/sigar-loader Kamon sigar-loader as java agent: `java -javaagent:/path/to/sigar-loader.jar`
  16. # Kamon sigar loader agent will extract and load sigar library during JVM start.
  17. #
  18. # 3) Place `sigar.jar` on the `classpath` and sigar native library for the o/s on the `java.library.path`
  19. # User is required to manage both project dependency and library deployment manually.
  20.  
  21. # Cluster metrics extension.
  22. # Provides periodic statistics collection and publication throughout the cluster.
  23. akka.cluster.metrics {
  24. # Full path of dispatcher configuration key.
  25. # Use "" for default key `akka.actor.default-dispatcher`.
  26. dispatcher = ""
  27. # How long should any actor wait before starting the periodic tasks.
  28. periodic-tasks-initial-delay = 1s
  29. # Sigar native library extract location.
  30. # Use per-application-instance scoped location, such as program working directory.
  31. native-library-extract-folder = ${user.dir}"/native"
  32. # Metrics supervisor actor.
  33. supervisor {
  34. # Actor name. Example name space: /system/cluster-metrics
  35. name = "cluster-metrics"
  36. # Supervision strategy.
  37. strategy {
  38. #
  39. # FQCN of class providing `akka.actor.SupervisorStrategy`.
  40. # Must have a constructor with signature `(com.typesafe.config.Config)`.
  41. # Default metrics strategy provider is a configurable extension of `OneForOneStrategy`.
  42. provider = "akka.cluster.metrics.ClusterMetricsStrategy"
  43. #
  44. # Configuration of the default strategy provider.
  45. # Replace with custom settings when overriding the provider.
  46. configuration = {
  47. # Log restart attempts.
  48. loggingEnabled = true
  49. # Child actor restart-on-failure window.
  50. withinTimeRange = 3s
  51. # Maximum number of restart attempts before child actor is stopped.
  52. maxNrOfRetries = 3
  53. }
  54. }
  55. }
  56. # Metrics collector actor.
  57. collector {
  58. # Enable or disable metrics collector for load-balancing nodes.
  59. # Metrics collection can also be controlled at runtime by sending control messages
  60. # to /system/cluster-metrics actor: `akka.cluster.metrics.{CollectionStartMessage,CollectionStopMessage}`
  61. enabled = on
  62. # FQCN of the metrics collector implementation.
  63. # It must implement `akka.cluster.metrics.MetricsCollector` and
  64. # have public constructor with akka.actor.ActorSystem parameter.
  65. # Will try to load in the following order of priority:
  66. # 1) configured custom collector 2) internal `SigarMetricsCollector` 3) internal `JmxMetricsCollector`
  67. provider = ""
  68. # Try all 3 available collector providers, or else fail on the configured custom collector provider.
  69. fallback = true
  70. # How often metrics are sampled on a node.
  71. # Shorter interval will collect the metrics more often.
  72. # Also controls frequency of the metrics publication to the node system event bus.
  73. sample-interval = 3s
  74. # How often a node publishes metrics information to the other nodes in the cluster.
  75. # Shorter interval will publish the metrics gossip more often.
  76. gossip-interval = 3s
  77. # How quickly the exponential weighting of past data is decayed compared to
  78. # new data. Set lower to increase the bias toward newer values.
  79. # The relevance of each data sample is halved for every passing half-life
  80. # duration, i.e. after 4 times the half-life, a data sample’s relevance is
  81. # reduced to 6% of its original relevance. The initial relevance of a data
  82. # sample is given by 1 – 0.5 ^ (collect-interval / half-life).
  83. # See http://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
  84. moving-average-half-life = 12s
  85. }
  86. }
  87.  
  88. # Cluster metrics extension serializers and routers.
  89. akka.actor {
  90. # Protobuf serializer for remote cluster metrics messages.
  91. serializers {
  92. akka-cluster-metrics = "akka.cluster.metrics.protobuf.MessageSerializer"
  93. }
  94. # Interface binding for remote cluster metrics messages.
  95. serialization-bindings {
  96. "akka.cluster.metrics.ClusterMetricsMessage" = akka-cluster-metrics
  97. }
  98. # Globally unique metrics extension serializer identifier.
  99. serialization-identifiers {
  100. "akka.cluster.metrics.protobuf.MessageSerializer" = 10
  101. }
  102. # Provide routing of messages based on cluster metrics.
  103. router.type-mapping {
  104. cluster-metrics-adaptive-pool = "akka.cluster.metrics.AdaptiveLoadBalancingPool"
  105. cluster-metrics-adaptive-group = "akka.cluster.metrics.AdaptiveLoadBalancingGroup"
  106. }
  107. }

akka-cluster-tools ~~~~~~~~~~~~------

  1. ############################################
  2. # Akka Cluster Tools Reference Config File #
  3. ############################################
  4.  
  5. # This is the reference config file that contains all the default settings.
  6. # Make your edits/overrides in your application.conf.
  7.  
  8. # //#pub-sub-ext-config
  9. # Settings for the DistributedPubSub extension
  10. akka.cluster.pub-sub {
  11. # Actor name of the mediator actor, /system/distributedPubSubMediator
  12. name = distributedPubSubMediator
  13.  
  14. # Start the mediator on members tagged with this role.
  15. # All members are used if undefined or empty.
  16. role = ""
  17.  
  18. # The routing logic to use for 'Send'
  19. # Possible values: random, round-robin, broadcast
  20. routing-logic = random
  21.  
  22. # How often the DistributedPubSubMediator should send out gossip information
  23. gossip-interval = 1s
  24.  
  25. # Removed entries are pruned after this duration
  26. removed-time-to-live = 120s
  27.  
  28. # Maximum number of elements to transfer in one message when synchronizing the registries.
  29. # Next chunk will be transferred in next round of gossip.
  30. max-delta-elements = 3000
  31. # The id of the dispatcher to use for DistributedPubSubMediator actors.
  32. # If not specified default dispatcher is used.
  33. # If specified you need to define the settings of the actual dispatcher.
  34. use-dispatcher = ""
  35.  
  36. }
  37. # //#pub-sub-ext-config
  38.  
  39. # Protobuf serializer for cluster DistributedPubSubMeditor messages
  40. akka.actor {
  41. serializers {
  42. akka-pubsub = "akka.cluster.pubsub.protobuf.DistributedPubSubMessageSerializer"
  43. }
  44. serialization-bindings {
  45. "akka.cluster.pubsub.DistributedPubSubMessage" = akka-pubsub
  46. }
  47. serialization-identifiers {
  48. "akka.cluster.pubsub.protobuf.DistributedPubSubMessageSerializer" = 9
  49. }
  50. }
  51.  
  52.  
  53. # //#receptionist-ext-config
  54. # Settings for the ClusterClientReceptionist extension
  55. akka.cluster.client.receptionist {
  56. # Actor name of the ClusterReceptionist actor, /system/receptionist
  57. name = receptionist
  58.  
  59. # Start the receptionist on members tagged with this role.
  60. # All members are used if undefined or empty.
  61. role = ""
  62.  
  63. # The receptionist will send this number of contact points to the client
  64. number-of-contacts = 3
  65.  
  66. # The actor that tunnel response messages to the client will be stopped
  67. # after this time of inactivity.
  68. response-tunnel-receive-timeout = 30s
  69. # The id of the dispatcher to use for ClusterReceptionist actors.
  70. # If not specified default dispatcher is used.
  71. # If specified you need to define the settings of the actual dispatcher.
  72. use-dispatcher = ""
  73. }
  74. # //#receptionist-ext-config
  75.  
  76. # //#cluster-client-config
  77. # Settings for the ClusterClient
  78. akka.cluster.client {
  79. # Actor paths of the ClusterReceptionist actors on the servers (cluster nodes)
  80. # that the client will try to contact initially. It is mandatory to specify
  81. # at least one initial contact.
  82. # Comma separated full actor paths defined by a string on the form of
  83. # "akka.tcp://system@hostname:port/system/receptionist"
  84. initial-contacts = []
  85. # Interval at which the client retries to establish contact with one of
  86. # ClusterReceptionist on the servers (cluster nodes)
  87. establishing-get-contacts-interval = 3s
  88. # Interval at which the client will ask the ClusterReceptionist for
  89. # new contact points to be used for next reconnect.
  90. refresh-contacts-interval = 60s
  91. # How often failure detection heartbeat messages should be sent
  92. heartbeat-interval = 2s
  93. # Number of potentially lost/delayed heartbeats that will be
  94. # accepted before considering it to be an anomaly.
  95. # The ClusterClient is using the akka.remote.DeadlineFailureDetector, which
  96. # will trigger if there are no heartbeats within the duration
  97. # heartbeat-interval + acceptable-heartbeat-pause, i.e. 15 seconds with
  98. # the default settings.
  99. acceptable-heartbeat-pause = 13s
  100. # If connection to the receptionist is not established the client will buffer
  101. # this number of messages and deliver them the connection is established.
  102. # When the buffer is full old messages will be dropped when new messages are sent
  103. # via the client. Use 0 to disable buffering, i.e. messages will be dropped
  104. # immediately if the location of the singleton is unknown.
  105. # Maximum allowed buffer size is 10000.
  106. buffer-size = 1000
  107. }
  108. # //#cluster-client-config
  109.  
  110. # Protobuf serializer for ClusterClient messages
  111. akka.actor {
  112. serializers {
  113. akka-cluster-client = "akka.cluster.client.protobuf.ClusterClientMessageSerializer"
  114. }
  115. serialization-bindings {
  116. "akka.cluster.client.ClusterClientMessage" = akka-cluster-client
  117. }
  118. serialization-identifiers {
  119. "akka.cluster.client.protobuf.ClusterClientMessageSerializer" = 15
  120. }
  121. }
  122.  
  123. # //#singleton-config
  124. akka.cluster.singleton {
  125. # The actor name of the child singleton actor.
  126. singleton-name = "singleton"
  127. # Singleton among the nodes tagged with specified role.
  128. # If the role is not specified it's a singleton among all nodes in the cluster.
  129. role = ""
  130. # When a node is becoming oldest it sends hand-over request to previous oldest,
  131. # that might be leaving the cluster. This is retried with this interval until
  132. # the previous oldest confirms that the hand over has started or the previous
  133. # oldest member is removed from the cluster (+ akka.cluster.down-removal-margin).
  134. hand-over-retry-interval = 1s
  135. # The number of retries are derived from hand-over-retry-interval and
  136. # akka.cluster.down-removal-margin (or ClusterSingletonManagerSettings.removalMargin),
  137. # but it will never be less than this property.
  138. min-number-of-hand-over-retries = 10
  139. }
  140. # //#singleton-config
  141.  
  142. # //#singleton-proxy-config
  143. akka.cluster.singleton-proxy {
  144. # The actor name of the singleton actor that is started by the ClusterSingletonManager
  145. singleton-name = ${akka.cluster.singleton.singleton-name}
  146. # The role of the cluster nodes where the singleton can be deployed.
  147. # If the role is not specified then any node will do.
  148. role = ""
  149. # Interval at which the proxy will try to resolve the singleton instance.
  150. singleton-identification-interval = 1s
  151. # If the location of the singleton is unknown the proxy will buffer this
  152. # number of messages and deliver them when the singleton is identified.
  153. # When the buffer is full old messages will be dropped when new messages are
  154. # sent via the proxy.
  155. # Use 0 to disable buffering, i.e. messages will be dropped immediately if
  156. # the location of the singleton is unknown.
  157. # Maximum allowed buffer size is 10000.
  158. buffer-size = 1000
  159. }
  160. # //#singleton-proxy-config
  161.  
  162. # Serializer for cluster ClusterSingleton messages
  163. akka.actor {
  164. serializers {
  165. akka-singleton = "akka.cluster.singleton.protobuf.ClusterSingletonMessageSerializer"
  166. }
  167. serialization-bindings {
  168. "akka.cluster.singleton.ClusterSingletonMessage" = akka-singleton
  169. }
  170. serialization-identifiers {
  171. "akka.cluster.singleton.protobuf.ClusterSingletonMessageSerializer" = 14
  172. }
  173. }

akka-cluster-sharding ~~~~~~~~~~~~---------

  1. ###############################################
  2. # Akka Cluster Sharding Reference Config File #
  3. ###############################################
  4.  
  5. # This is the reference config file that contains all the default settings.
  6. # Make your edits/overrides in your application.conf.
  7.  
  8.  
  9. # //#sharding-ext-config
  10. # Settings for the ClusterShardingExtension
  11. akka.cluster.sharding {
  12.  
  13. # The extension creates a top level actor with this name in top level system scope,
  14. # e.g. '/system/sharding'
  15. guardian-name = sharding
  16.  
  17. # Specifies that entities runs on cluster nodes with a specific role.
  18. # If the role is not specified (or empty) all nodes in the cluster are used.
  19. role = ""
  20.  
  21. # When this is set to 'on' the active entity actors will automatically be restarted
  22. # upon Shard restart. i.e. if the Shard is started on a different ShardRegion
  23. # due to rebalance or crash.
  24. remember-entities = off
  25.  
  26. # If the coordinator can't store state changes it will be stopped
  27. # and started again after this duration, with an exponential back-off
  28. # of up to 5 times this duration.
  29. coordinator-failure-backoff = 5 s
  30.  
  31. # The ShardRegion retries registration and shard location requests to the
  32. # ShardCoordinator with this interval if it does not reply.
  33. retry-interval = 2 s
  34.  
  35. # Maximum number of messages that are buffered by a ShardRegion actor.
  36. buffer-size = 100000
  37.  
  38. # Timeout of the shard rebalancing process.
  39. handoff-timeout = 60 s
  40.  
  41. # Time given to a region to acknowledge it's hosting a shard.
  42. shard-start-timeout = 10 s
  43.  
  44. # If the shard is remembering entities and can't store state changes
  45. # will be stopped and then started again after this duration. Any messages
  46. # sent to an affected entity may be lost in this process.
  47. shard-failure-backoff = 10 s
  48.  
  49. # If the shard is remembering entities and an entity stops itself without
  50. # using passivate. The entity will be restarted after this duration or when
  51. # the next message for it is received, which ever occurs first.
  52. entity-restart-backoff = 10 s
  53.  
  54. # Rebalance check is performed periodically with this interval.
  55. rebalance-interval = 10 s
  56.  
  57. # Absolute path to the journal plugin configuration entity that is to be
  58. # used for the internal persistence of ClusterSharding. If not defined
  59. # the default journal plugin is used. Note that this is not related to
  60. # persistence used by the entity actors.
  61. journal-plugin-id = ""
  62.  
  63. # Absolute path to the snapshot plugin configuration entity that is to be
  64. # used for the internal persistence of ClusterSharding. If not defined
  65. # the default snapshot plugin is used. Note that this is not related to
  66. # persistence used by the entity actors.
  67. snapshot-plugin-id = ""
  68.  
  69. # Parameter which determines how the coordinator will be store a state
  70. # valid values either "persistence" or "ddata"
  71. # The "ddata" mode is experimental, since it depends on the experimental
  72. # module akka-distributed-data-experimental.
  73. state-store-mode = "persistence"
  74.  
  75. # The shard saves persistent snapshots after this number of persistent
  76. # events. Snapshots are used to reduce recovery times.
  77. snapshot-after = 1000
  78.  
  79. # Setting for the default shard allocation strategy
  80. least-shard-allocation-strategy {
  81. # Threshold of how large the difference between most and least number of
  82. # allocated shards must be to begin the rebalancing.
  83. rebalance-threshold = 10
  84.  
  85. # The number of ongoing rebalancing processes is limited to this number.
  86. max-simultaneous-rebalance = 3
  87. }
  88.  
  89. # Timeout of waiting the initial distributed state (an initial state will be queried again if the timeout happened)
  90. # works only for state-store-mode = "ddata"
  91. waiting-for-state-timeout = 5 s
  92.  
  93. # Timeout of waiting for update the distributed state (update will be retried if the timeout happened)
  94. # works only for state-store-mode = "ddata"
  95. updating-state-timeout = 5 s
  96.  
  97. # Settings for the coordinator singleton. Same layout as akka.cluster.singleton.
  98. coordinator-singleton = ${akka.cluster.singleton}
  99.  
  100. # The id of the dispatcher to use for ClusterSharding actors.
  101. # If not specified default dispatcher is used.
  102. # If specified you need to define the settings of the actual dispatcher.
  103. # This dispatcher for the entity actors is defined by the user provided
  104. # Props, i.e. this dispatcher is not used for the entity actors.
  105. use-dispatcher = ""
  106. }
  107. # //#sharding-ext-config
  108.  
  109.  
  110. # Protobuf serializer for Cluster Sharding messages
  111. akka.actor {
  112. serializers {
  113. akka-sharding = "akka.cluster.sharding.protobuf.ClusterShardingMessageSerializer"
  114. }
  115. serialization-bindings {
  116. "akka.cluster.sharding.ClusterShardingSerializable" = akka-sharding
  117. }
  118. serialization-identifiers {
  119. "akka.cluster.sharding.protobuf.ClusterShardingMessageSerializer" = 13
  120. }
  121. }

akka-distributed-data ~~~~~~~~~~~~---------

  1. ##############################################
  2. # Akka Distributed DataReference Config File #
  3. ##############################################
  4.  
  5. # This is the reference config file that contains all the default settings.
  6. # Make your edits/overrides in your application.conf.
  7.  
  8.  
  9. #//#distributed-data
  10. # Settings for the DistributedData extension
  11. akka.cluster.distributed-data {
  12. # Actor name of the Replicator actor, /system/ddataReplicator
  13. name = ddataReplicator
  14.  
  15. # Replicas are running on members tagged with this role.
  16. # All members are used if undefined or empty.
  17. role = ""
  18.  
  19. # How often the Replicator should send out gossip information
  20. gossip-interval = 2 s
  21.  
  22. # How often the subscribers will be notified of changes, if any
  23. notify-subscribers-interval = 500 ms
  24.  
  25. # Maximum number of entries to transfer in one gossip message when synchronizing
  26. # the replicas. Next chunk will be transferred in next round of gossip.
  27. max-delta-elements = 1000
  28. # The id of the dispatcher to use for Replicator actors. If not specified
  29. # default dispatcher is used.
  30. # If specified you need to define the settings of the actual dispatcher.
  31. use-dispatcher = ""
  32.  
  33. # How often the Replicator checks for pruning of data associated with
  34. # removed cluster nodes.
  35. pruning-interval = 30 s
  36. # How long time it takes (worst case) to spread the data to all other replica nodes.
  37. # This is used when initiating and completing the pruning process of data associated
  38. # with removed cluster nodes. The time measurement is stopped when any replica is
  39. # unreachable, so it should be configured to worst case in a healthy cluster.
  40. max-pruning-dissemination = 60 s
  41. # Serialized Write and Read messages are cached when they are sent to
  42. # several nodes. If no further activity they are removed from the cache
  43. # after this duration.
  44. serializer-cache-time-to-live = 10s
  45. }
  46. #//#distributed-data
  47.  
  48. # Protobuf serializer for cluster DistributedData messages
  49. akka.actor {
  50. serializers {
  51. akka-data-replication = "akka.cluster.ddata.protobuf.ReplicatorMessageSerializer"
  52. akka-replicated-data = "akka.cluster.ddata.protobuf.ReplicatedDataSerializer"
  53. }
  54. serialization-bindings {
  55. "akka.cluster.ddata.Replicator$ReplicatorMessage" = akka-data-replication
  56. "akka.cluster.ddata.ReplicatedDataSerialization" = akka-replicated-data
  57. }
  58. serialization-identifiers {
  59. "akka.cluster.ddata.protobuf.ReplicatedDataSerializer" = 11
  60. "akka.cluster.ddata.protobuf.ReplicatorMessageSerializer" = 12
  61. }
  62. }

Актори

Модель акторів провадить вищий рівень абстракції для написання конкурентних та розподілених систем. Він вивільнює розробника від явного керування блокуваннями та потоками, спрощуючи написання конкурентних та паралельних систем. Актори були визначені в 1973 році в документі Карла Хьювіта, але стали популярними тільки з мовою Erlang, та з виликим успіхом використовувались як приклад на Ericsson для побудови висококонкурентних та надійних систем телекома.

API акторів Akka подібне до акторів Scala, що були позичені в частині синтаксису з Erlang.

Створення акторів

Зауваження

Оскільки Akka примушує до батьківського нагляду кожного актора, та (потенційно) кожний актор є наглядачем за дітьми, дуже бажано, щоб ви ознайомились з Системами акторів та Нагляд та мониторинг, а також на допомогу може прийти читання Посилання на акторів, шляхи та адреси.

Визначення класу актора

Актори реалізуються поширенням базового трейту Actor, та реалізацією методаreceive. Метод receiveповинен визначати серію перевірок case (що має тип PartialFunction[Any, Unit]), що визначає, які повідомлення може обробляти цей актор, використовуючи стандартне співпадіння шаблонів Scala, разом з тим, як ці повідомлення мають бути оброблені.

Ось приклад:

  1. import akka.actor.Actor
  2. import akka.actor.Props
  3. import akka.event.Logging
  4.  
  5. class MyActor extends Actor {
  6. val log = Logging(context.system, this)
  7.  
  8. def receive = {
  9. case "test" => log.info("received test")
  10. case _ => log.info("received unknown message")
  11. }
  12. }

Будь ласка, зауважте, що цикл повідомлень Akka receive є вичерпним, що має відмінність до Erlang, та останніми Scala Actors. Це означає, що вам треба провадити співпадіння шаблонів для всіх повідомлень, що ви можете сприймати, якщо ви бажаєте бути в змозі обробляти невідомі повідомлення, та коли вам треба мати випадок по замовчанню, як в прикладі вище. В іншому випадку буде опублікований akka.actor.UnhandledMessage(message, sender, recipient) в ActorSystem'sEventStream.

Надалі занотуйте, що тип результата поведінки, визначеної вище, є Unit; якщо актор буде відповідати на надіслане повідомлення, це має бути зроблене явно, як показане нижче.

Результат метода receive є об'єкт часткової функції, що зберігається з актором як “внутрішня поведінка”, дивіться Become/Unbecome для подальшої інформації щодо зміни поведінки актора після створення.

Props

Props є клас конфігурації, щоб задати опції для створення акторів, думайте про це як про незмінний, і, таким чином, розподілений рецепт для створення актора, включаючи асоційовану інформацію по розгортанню (тобто, який диспечер використовується, дивіться нижче). Ось деякі приклади того, як створити примірник Props.

  1. import akka.actor.Props
  2.  
  3. val props1 = Props[MyActor]
  4. val props2 = Props(new ActorWithArgs("arg")) // уважно, дивіться нижче
  5. val props3 = Props(classOf[ActorWithArgs], "arg")

Другий варіант показує, як передавати аргументи конструктора до створюваного Actor, але це треба використовувати тільки за межами акторів, як пояснено нижче.

Останній рядок показує можливість передавати аргументи конструктора, безвідносно до контексту, де він використовується. Присутність відповідного конструктора перевіряється під час створення об'єктаProps, що призводить до IllegalArgumentException, якщо конструктор не буде знайдений, або буде знайдено більше одного.

Небезпечні варіанти

  1. // НЕ РЕКОМЕНДОВАНЕ в іншому акторі:
  2. // заохочує замикання замикаючого класа
  3. val props7 = Props(new MyActor)

Цей метод не рекомендований для використання в іншому акторі, бо він заохочує на оточуєму полі зору, що призводить до несеріалізуємого Props та можливого стану гонок (що руйнує енкапсуляцію актора). Ми будемо провадити базоване на макро рішення в подальшому релізі, що дозволить подібний синтаксис без головняка, та на той час попереднє рішення піде у відставку. З іншого боку, використання цього варіанту в фабриці Props в об'єкті-компанйоні актора, як документовано в “Рекомендованих практиках” нижче, є досить прийнятним.

Існує два випадка використання для ціх методів: передання аргументів конструктора до актора — що вирішене недавно введеним методом Props.apply(clazz, args)вище, або рекомендована практика нижче — та створення акторів “по місцю” як анонімних класів. Останнє має бути вирішене, роблячи ці актори іменованими класами (якщо вони не декларовані на object вищого рівня, коли замикаючий примірник посиланняthis треба передати як перший аргумент).

Попередження

Визначення одного актора в іншому є дуже небезпечним, та руйнує інкапсуляцію акторів. Ніколи не передавайте посилання актора this до Props!

Створення акторів з Props

Актори можуть бути створені з передачею примірника Props до метода-фабрики actorOf, що доступне наonActorSystem та ActorContext.

  1. import akka.actor.ActorSystem
  2.  
  3. // ActorSystem є важким об'єктом: створюйте тільки один на застосування
  4. val system = ActorSystem("mySystem")
  5. val myActor = system.actorOf(Props[MyActor], "myactor2")

Використання ActorSystem буде створювати акторів вищого рівня, за якими наглядає актор-захисник, наданий смстемою акторів, тоді як використання контексту акторів створить актора-дитинча.

  1. class FirstActor extends Actor {
  2. val child = context.actorOf(Props[MyActor], name = "myChild")
  3. // деяка поведінка ...
  4. }

Рекомендовано створювати ієрархію дітей, дітей дітей, і так далі, щоб це пасувало до логічної структури обробки збоїв застосування, також дивіться  Системи акторів.

Виклик до actorOf повертає примірник ActorRef. Це посилання на примірник актора, та є єдиним способом взаємодіяти з ним. ActorRef є незмінним, та має відношення один до одного з акторами, які він представляє. ActorRef також серіалізується та є обізнаним з мережею. Це означає, що ви можете серіалізувати його, надіслати по дроті, та використати на віддаленому вузлі, та він буде представляти той самий Actor на оригінальному вузлі, навіть по мережі.

Іменований параметр є опціональним, але ви маєте переважно іменувати ваших акторів, оскільки це використовується в повідомленнях журналу для ідентифікації акторів. Ім'я мусить не бути порожнім, або починатись з $, але воно може вістити URL-закодовані символи (наприклад, %20для проміжку). Якщо надане ім'я вже використовується іншим актором-дитям того ж батька, викликаєтьсяInvalidActorNameException.

Актори автоматично асинхронно стартують після створення.

Впровадження залежностей (DI)

Якщо ваш актор має конструктор, що приймає параметри, тоді вони мають бути також і частиною Props, як описане вище. Аде є випадки, коли має використовуватись метод-фабрика, наприклад, коли справжні аргументи конструктора визначаються через фреймворк впровадження залежностей.

  1. import akka.actor.IndirectActorProducer
  2.  
  3. class DependencyInjector(applicationContext: AnyRef, beanName: String)
  4. extends IndirectActorProducer {
  5.  
  6. override def actorClass = classOf[Actor]
  7. override def produce =
  8. // отримати свіжий примірник Actor з фреймворку DI ...
  9. }
  10.  
  11. val actorRef = system.actorOf(
  12. Props(classOf[DependencyInjector], applicationContext, "hello"),
  13. "helloBean")

Попередження

Ви можете мати спокусу часом запропонувати IndirectActorProducer, що завжди повертає той же примірник, тобто використовувати lazy val. Це не підтримується, бо це іде всупереч ідеї рестарта актора, що описане тут: Що значить рестарт.

Коли використовується фреймворк впровадження залежностей, біни акторів НЕ ПОВИННІ мати оглядовість синглтона.

Технології впровадження залежностей та інтеграція з фреймворками впровадження залежностей більш глибоко описані в інструкції Використанні Akka з впровадженням залежностей, та туторіалі Akka Java Spring в Lightbend Activator.

Поштова скринька Входящі

Коли ви пишете код за межами акторів, що буде комунікувати з акторами, шаблонask може бути одним з рішень (дивіться нижче), але ж дві речі, що він не може зробити: відіслати декілька відповідей (тобто, підписатиActorRef на сервіс повідомлень), та слідкувати за життєвим циклом іншого актора. Для цього існує класInbox:

  1. implicit val i = inbox()
  2. echo ! "hello"
  3. i.receive() should ===("hello")

Є неявне перетворення з поштової скриньки на посилання до актора, що означає, що в цьому прикладі посилання надсилача буде приховане в поштовій скринці актора. Це дозволяє відповісти в останньому рядку. Нагляд за актором також простий:

  1. val target = // деякий актор
  2. val i = inbox()
  3. i watch target

Actor API

Трейт Actor визначає тільки один абстрактний метод, вже вказаний receive, що реалізує поведінку актора.

Якщо поточна поведінка актора не співпадає з отрисанним повідомленням, викликається unhandled, що по замовчанню публікує на потоці системи акторів akka.actor.UnhandledMessage(message, sender, recipient) (встановіть елемент конфігурації akka.actor.debug.unhandled в on, щоб перетворювати їх на справжні повідомлення Debug).

На додаток це дає:

  • self посилання до ActorRef актора

  • sender посилання на актора-надсилача останнього отриманого повідомлення, зазвичай використовується як описане в Відповідь на повідомлення

  • supervisorStrategy перезаписуване користувачем визначення стратегії, що буде застосовуватись для нагляду за акторами-дітьми

    Ця стратегія звичайно визначається в акторі, щоб мати доступ  до внутрішнього стану актора в функції  прийняття рішення: оскільки відмова комунікується як повідомлення, надіслане супервізору, та обробляється як любі інші повідомлення (хоча і за межами звичайної поведінки), всі значення та змінні в акторі є доступними, як і посиланняsender (що буде безпосереднім дитям, що сповіщає про збій; якщо збій відбувся в більш віддаленому нащадку, він буде все одне повідомлятись на один рівень за раз).

  • context викриває контекстуальну інформацію для актора, та поточне повідомлення, таким чином:

    • методи-фабрики для створення акторів (actorOf)
    • система, що якої належить актор
    • батьківський наглядач
    • дитя, що наглядається
    • моніторинг життєвого циклу
    • гарача заміна стеку поведінки, як описане в Become/Unbecome

Ви можете імпортувати члени в context, щоб уникнути префіксів доступу доcontext.

  1. class FirstActor extends Actor {
  2. import context._
  3. val myActor = actorOf(Props[MyActor], name = "myactor")
  4. def receive = {
  5. case x => myActor ! x
  6. }
  7. }

Інші видимі методи є переписувані користувачем перехоплення життєвого циклу, що описуються наступним чином:

  1. def preStart(): Unit = ()
  2.  
  3. def postStop(): Unit = ()
  4.  
  5. def preRestart(reason: Throwable, message: Option[Any]): Unit = {
  6. context.children foreach { child
  7. context.unwatch(child)
  8. context.stop(child)
  9. }
  10. postStop()
  11. }
  12.  
  13. def postRestart(reason: Throwable): Unit = {
  14. preStart()
  15. }

Показана вище реалізація по замовчанню провадиться трейтом Actor.

Життєвий цикл актора

../_images/actor_lifecycle1.png

Шлях в системі акторів представляє "місце ", що може бути зайняте проживаючим актором. На початку (окремо від ініційованих системою акторів) шлях пустий. Коли викликається actorOf(), він присвоює інкарнацію актора, описаного в переданомуProps до вказаного шляха. Інкарнація актора ідентифікується по шляху та UID. Рестарт тільки замінює примірник Actor, визначений в Props, але інкарнація, та, таким чином UID, залишаються тими самими.

Життєвий цикл інкарнації завершується, коли актор зупиняється. В цій точці викликаються відповідні події життєвого цикла, та наглядаючий актор повідомляється про завершення. Після того, як інкарнація зупинилась, шлях може бути знову використаний для створення актора за допомогою actorOf(). В цьому випадку ім'я нової інкарнації буде таким самим, що і в попередньої, але UID буде відрізнятись. Актор може бути зупинений самим актором, іншим актором, абоActorSystem (дивіться Зупинка акторів).

Зауваження

Важливо зауважити, що Actors не зупиняється автоматично, коли на нього немає посилань, кожний створений Actor має бути також явно знищений. Одне спрощення полягає в тому, що зупинення батьківського актора також рекурсивно зупиняє всі дитячі актори, що створив цей батько.

ActorRef завжди представляє інкарнацію (шлях та UID), не тільки даний шлях. Таким чином, якщо актор зупиняєтсья, та створюється новий з тим же ім'ям, ActorRef старої інкарнації не буде вказувати на нову.

ActorSelection, з іншого боку, вказує на шлях (або декілька шляхів, якщо вказана зірочка), та абсолютно не звертаючи увагу, на яку інкарнацію, що наразі займає цей шлях. ActorSelection не може відслідковуватись з декількох причин. Можливо розрішити ActorRef поточної інкарнації, що проживає за шляхом, надсилаючи повідомлення Identify доActorSelection, що поверне ActorIdentity, що міститиме коректне посилання (дивіться Ідентифікація акторів через Actor Selection). Це також можна зробити за допомогою метода resolveOne на ActorSelection, що повертає Future співпадаючих ActorRef.

Моніторинг життєвого циклу, aka DeathWatch

Щоб отримувати повідомлення коли інший актор завершується (тобто, зупиняється назавжди, не тимчасово дає збій та рестартує), актор може зареєструвати себе для отримання повідомлення Terminated, що надсилається іншим актором під час завершення (дивіться Зупинка акторів). Ця послуга провадиться компонентом DeathWatch системи акторів.

Реєстрація монітора є простою:

  1. import akka.actor.{ Actor, Props, Terminated }
  2.  
  3. class WatchActor extends Actor {
  4. val child = context.actorOf(Props.empty, "child")
  5. context.watch(child) // <-- це єдиний виклик, що треба для реєсрації
  6. var lastSender = context.system.deadLetters
  7.  
  8. def receive = {
  9. case "kill" =>
  10. context.stop(child); lastSender = sender()
  11. case Terminated(`child`) => lastSender ! "finished"
  12. }
  13. }

Треба зауважити, що повідомлення Terminated генерується незалежно від порядку, в якому відбувається реєстрація та завершення. Зокрема, наглядаючий актор отримає повідомлення Terminated, навіть якщо нагляданий актор вже завершений під час реєстрації.

Реєстрація декілька разів не обов'язково призведе до генерації декількох повідомлень, але немає гарантії, що тільки рівно одне повідомлення буде отримане: якщо завершення нагляданого актора було згенероване та поставлене в чергу, та відбулась інша реєстрація перед отробкою повідомлення, тоді друге повідомлення буде поставлене в чергу, оскільки реєстрація для мониторинга вже завершеного актора призводить до безпосередньої генерації повідомлення Terminated.

Також можливо відреєструватись з нагляду життездатності іншого актора, викоистовуючи context.unwatch(target). Це робить, навіть якщо повідомлення  Terminated вже було поставлене до черги в поштову скриньку; після виклику unwatch жодних повідомлень Terminated для цього актора більше не буде оброблене.

Перехоплювач старту

Зразу після запуску актора викликається його метод preStart.

  1. override def preStart() {
  2. child = context.actorOf(Props[MyActor], "child")
  3. }

Цей метод викликається, коли актор створюється перший раз. Під час рестарту він викликається реалізацією по замовчаннюpostRestart, що означає, що переписуючи цей метод ви можете обрати, чи код ініціалізації цього метода викликається тільки рівно один раз для цього актора, або кожний рестарт. Код ініціалізації, що є частиною конструктора актора, буде завжди викликатись, коли створюється примірник класа актора, що відбувається кожний рестарт.

Перехоплювач рестарту

Всі актори наглядаються, тобто приєднані до іншого актора зі стратегією обробки відмови. Актори можуть бути рестартовані, в випадку виникнення виключення під час обробки повідомлення (дивіться Нагляд та мониторинг). Цей рестарт включає наступні перелічені перехоплення:

  1. Старий актор інформується викликом preRestart з виключенням, яке призвело до рестарту, та повідомленням, що викликало це виключення; останнє може бути None, якщо рестарт не викликаний обробкою повідомлення, тобто, коли супервізор не перехопив виключення, та він рестартує за бажанням свого супервізора, або якщо актор рестартує через відмову однорівневого актора. Якщо повідомлення доступне, тоді і надсилач цього повідомлення доступне в звичайний спосіб (тобто, через виклик sender).

    Цей метод є кращим місцем для очистки, підготування почви для нового примірника актора, тощо. По замовчанню зупиняються всі діти, та викликається postStop.

  2. Використовується початковий виклик з actorOf для створення нового примірника.

  3. Викликається метод postRestart нового актора, з виключенням, що спричинило рестарт. По замовчанню викликається preStart, так само, як під час звичайного запуску.

Рестарт актора заміщує тільки дійсний примірник актора; вміст поштової скриньки не змінюється під час рестарту, так що обробка повідомлень буде відновлена після повернення з перехоплювача postRestart. Повідомлення, що спричинило виключення, не буде надіслане знову. Любе повідомлення, надіслане до актора під час рестарта, буде поставлене до черги в звичайний спосіб.

Попередження

Майте на увазі, що порядок повідомлень відмови відносно користувацьких повідомлень не визначений. Зокрема, батько може рестартувати свого дитя до того, як він обробить останнє повідомлення, надіслане дитиною до збою.  Дивіться Дискусія: порядок повідомлень щодо деталей.

Перехоплювач зупинки

Після зупинки актора викликається його перехоплювач postStop, що може бути використаний, наприклад, для дереєстрації цього актора від інших сервісів. Цей перехоплювач гарантовано виконується після відключення постановки повідомлень в чергу для цього актора, тобто, повідомлення, надіслані до зупиненого актора будуть переправлені до deadLetters поточної ActorSystem.

Ідентифікація акторів через Actor Selection

Як описане в Посиланнях на акторів, хляхи та адреси, кожний актор має унікальний логиіний шлях, що отримується слідуючи ланцюжку від дитини до батька , доки не буде досягникий корінь системи акторів, та це має фізичний шлях, що може відрізнятись, якщо ланцюжок супервізорів включає будь які віддплені супервізори. Ці шляхи використовуються системою для пошуку акторів, тобто, коли прийняте віддалене повідомлення, та шукається отримувач. Але вони також корисні і більш прямолінійно: актори можуть шукати інших акторів, вказуючи абсолютні або відносні шляхи — логічні або фізичні — та отримати у відповідь ActorSelection з результатом:

  1. // буде шукати абсолютний шлях
  2. context.actorSelection("/user/serviceA/aggregator")
  3. // буде шукати брата в того ж предка-супервізора
  4. context.actorSelection("../joe")

Зауваження

Завжди краще комунікувати з іншим актором з використанням його ActorRef, замість покладатись на ActorSelection. Виключенням може бути:

  • надсилання повідомлень з використанням можливості Доставки щонайменьше-раз
  • ініціалізвція першого контакту з віддаленою системою

В усіх інших випадках ActorRefs може бути запроваджений під час створення або ініціалізації актора, передаючи їх від батька дітям, або вводячи акторів, надсилаючи їх ActorRefs іншим акторам в повідомленнях.

Наданий шлях розбирається як java.net.URI, що, в основному, означає, що він розбивається по / на елементи шляху. Якщо шлях починається з  /, він є абсолютним, та пошук починається з кореневого охоронця (що є батьком "/user"); інакше він починається з поточного актора. Якщо елемент шляху дорівнює .., пошук зробить крок “догори”, в напрямку супервізора поточного актора, інакше він піде “донизу”, до вказаного дитя. Треба занотувати, що .. в шляхах акторів тут і надалі завжди означає логічну структуру, тобто, супервізор.

Елементи шляху селектора актора може містити узагальнуючі символи для широкомовлення повідомлень до цього розділу:

  1. // буде шукати всіх дітей serviceB з іменами, що починаються на worker
  2. context.actorSelection("/user/serviceB/worker*")
  3. // буде шукати всіх близнят того ж супервізора
  4. context.actorSelection("../*")

Повідомлення можуть бути надіслані через ActorSelection, та шлях  ActorSelection шукаються при доставленні кожного повідомлення. Якщо селектор не співпадає з жодним актором, повідомлення будуть відкинуті.

Щоб отримати ActorRef для ActorSelection, вам потрібно надіслати повідомлення до виборки, та використовувати посилання sender() відповіді від актора. Існує вбудоване повідомлення Identify, що розуміють всі актори, та автоматично відповідають на нього повідомленням ActorIdentity, що містить ActorRef. Це повідомлення обробляється зустрічними акторами особливим чином, в тому сенсі, що якщо пошук конкретного імені схибить (тобто, елемент шляху без зірочок не відповідають живому актору), тоді генерується негативний результат. Будь ласка, занотуйте, що це не означає, що доставлення цієї відповіді гарантовано, це все ще звичайне повідомлення.

  1. import akka.actor.{ Actor, Props, Identify, ActorIdentity, Terminated }
  2.  
  3. class Follower extends Actor {
  4. val identifyId = 1
  5. context.actorSelection("/user/another") ! Identify(identifyId)
  6.  
  7. def receive = {
  8. case ActorIdentity(`identifyId`, Some(ref)) =>
  9. context.watch(ref)
  10. context.become(active(ref))
  11. case ActorIdentity(`identifyId`, None) => context.stop(self)
  12.  
  13. }
  14.  
  15. def active(another: ActorRef): Actor.Receive = {
  16. case Terminated(`another`) => context.stop(self)
  17. }
  18. }

Ви також можете захопити ActorRef для ActorSelection за допомогою метода resolveOne на ActorSelection. Він повертає Future співпавшого ActorRef, якщо такий автор існує. Він завершається збоєм [[akka.actor.ActorNotFound]], якщо такого актора не існує, або ідентифікація не завершилась в наданий час таймаута.

Віддалені адреси акторів також можуть переглядатись, якщо дозволений ремоутинг remoting:

  1. context.actorSelection("akka.tcp://app@otherhost:1234/user/serviceB")

Приклад, що демонструє пошук актора, наданий в Прикладі ремоутинга.

Повідомлення та незмінність

ВАЖЛИВО: Повідомлення можуть бути любим типом об'єктів, але мають бути незмінними. Scala не можу примусити до незмінності (поки що), так що це повинно бути зроблене за домовленостю. Примітиви, як String, Int, Boolean завжди є незмінними. Окрім ціх, рекомендований підхід є використання кейс класів Scala, що є незмінними (якщо ви явно не викажете стан), та чудово робить зі співпадінням шаблонів на боці отримувача.

Ось приклад:

  1. // визначення кейс класа
  2. case class Register(user: User)
  3.  
  4. // створити нове повідомлення з кейс класа
  5. val message = Register(user)

Надсилання повідомлень

Повідомлення надсилаються акторові через один з наступних методів.

  • ! означає “надіслати-і-забути”, тобто, надіслати повідомлення асинхронно, та безпосередньо повернутись. Також відоме як tell.
  • ? надсилає повідомлення асинхронно, та повертає Future, що представляє можливу відповідь. Також відоме як ask.

Порядок повідомлень гарантований на основі кожного надсилача.

Зауваження

Є вплив на продуктивність з використанням ask, оскільки порібно дещо відстежувати на таймаут, дещо потрібне для пов'язання Promise з ActorRef, та це також повинне бути доступним через ремоутинг. Так що завжди схиляйтесь до tell в цілях продуктивності, та ask тільки якщо ви змушені.

Tell: надіслав-забув

Це переважний спосіб надсилати повідомлення. Немає блокуючого очікування повідомлення. Це надає кращу конкурентність та характеристики маштабованості.

  1. actorRef ! message

Якщо викликається з Actor, тоді посилання на актора, що надсилає, буде неявно передане разом з повідомленням, та доступне отримуючому акторовів його члені-методі sender(): ActorRef. Цільовий актор може використовувати це для відповіді оригінальному надсилачеві, використовуючи sender() ! replyMsg.

Якщо виклик походить з екземпляра, що не є Actor, надсилачем по замовчанню буде посилання на актораdeadLetters.

Ask: надіслати-та-повернути-Future

Шаблон ask включає акторів та майбутнє, таким чином він надається як шаблон використання, скоріше ніж метод на ActorRef:

  1. import akka.pattern.{ ask, pipe }
  2. import system.dispatcher // ExecutionContext, що буде використаний
  3. final case class Result(x: Int, s: String, d: Double)
  4. case object Request
  5.  
  6. implicit val timeout = Timeout(5 seconds) // потрібне для `?` нижче
  7.  
  8. val f: Future[Result] =
  9. for {
  10. x <- ask(actorA, Request).mapTo[Int] // прямий виклик предка
  11. s <- (actorB ask Request).mapTo[String] // виклик через неявне перетворення
  12. d <- (actorC ? Request).mapTo[Double] // виклик по символічному імені
  13. } yield Result(x, s, d)
  14.  
  15. f pipeTo actorD // .. або ..
  16. pipe(f) to actorD

Цей приклад демонструє ask разом з шаблоном на майбутньому pipeTo, тому що це буде, напевно, буде звичайною комбінацією. Будь ласка, зауважте, що все наведене вище повністю неблокуюче та асинхронне: ask продукує Future, троє з яких компонуються в нове мабйтнє з використанням for-осяжності, та потім pipeTo встановлює onComplete-обробник на майбутньому, щоб вплинути через надходження агрегованого Result на іншого актора.

Використання ask надісле пвідомлення до актора-отримувача, як і при tell, та актор-отримувач має відповісти за допомогою sender()! reply, щоб завершити повернуте Future зі значенням. Операція ask включає створення внутрішнього актора для обробки цієї відповіді, що потребує наявність таймаута, після чого він руйнується, щоб не прогавити ресурси; дивіться нижче щодо додаткової інформації.

Попередження

Щоб завершити майбутнє з виключенням, вам треба надіслати повідомлення Failure до надсилача. Це не робиться автоматично, коли актор підіймає виключення при обробці повідомлення.

  1. try {
  2. val result = operation()
  3. sender() ! result
  4. } catch {
  5. case e: Exception =>
  6. sender() ! akka.actor.Status.Failure(e)
  7. throw e
  8. }

Якщо актор не завершує майбутнє, він буде простроченний після періонду таймауту, завершуючи його з AskTimeoutException. Таймаут береться з одного з наступних розміщень в порядку приорітету:

  1. явно заданий таймаут, як в наступному коді:
  1. import scala.concurrent.duration._
  2. import akka.pattern.ask
  3. val future = myActor.ask("hello")(5 seconds)
  1. неявний аргумент типу akka.util.Timeout
  1. import scala.concurrent.duration._
  2. import akka.util.Timeout
  3. import akka.pattern.ask
  4. implicit val timeout = Timeout(5 seconds)
  5. val future = myActor ? "hello"

Дивіться Futures для додаткової інформації, щодо того, як очікувати запиту від майбутнього.

Методи onComplete, onSuccess, або onFailure наFuture можуть бути використані для реєстрації зворотнього виклику при завершенні Future, надаючи вам спосіб для уникання блокування.

Повідомлення

При використанні зворотніх викликів майбутнього, таких як onComplete onSuccess, та onFailure, в акторах вам потрібно уважно уникати замикань на посиланні точуючого актора, тобто, не викликайте методи, або не отримуйте доступ до змінного стану на отосуючому акторі з середини актора. Це може зруйнувати інкапсуляцію, та може призвести до багів синхронізації та стану гонок, оскільки звороній виклик буде запланований конекурентно з охоплюючим актором. На жаль, досі немає способу визначити ці нелегальні доступи під час компіляції. Таокж дивіться: Актори та розподілений змінний стан

Пересилання повідомлень

Ви можете пересилати повідомлення від одного актора іншому. Це означає, що оригінальна адреса/посилання надсилача буде збережена, навіть якщо повідомлення перейде через 'посередника'. Це може бути корисним при написанні акторів, що роблять як маршрутизатори, балансувальники навантаження, реплікатори, тощо.

  1. target forward message

Отримання повідомлень

Актор має реалізувати метод receive для отримання повідомлень:

  1. type Receive = PartialFunction[Any, Unit]
  2.  
  3. def receive: Actor.Receive

Цей метод повертає PartialFunction, тобто вираз ‘match/case’, в якому повідомлення може бути порівняне з різними випадками з використанням співпадінь Scala. Ось приклад цього:

  1. import akka.actor.Actor
  2. import akka.actor.Props
  3. import akka.event.Logging
  4.  
  5. class MyActor extends Actor {
  6. val log = Logging(context.system, this)
  7.  
  8. def receive = {
  9. case "test" => log.info("received test")
  10. case _ => log.info("received unknown message")
  11. }
  12. }

Відповідь на повідомлення

Якщо ви бажаєте обробити та відповісти на повідомлення, ви можете використати sender(), що поверне ActorRef. Ви можете відповісти надсилаючи ActorRef за допомогою sender() ! replyMsg. Ви також можете зберігти ActorRef для пізнішої відповіді, або передати посилання іншому актору. Якщо немає надсилача (повідомлення було надіслане без актора або майбутнього контексту), тоді надсилач по замовчанню є посилання на актора 'dead-letter'.

  1. case request =>
  2. val result = process(request)
  3. sender() ! result // буде мати актора dead-letter по замовчанню

Таймаут отримання

ActorContext setReceiveTimeout визначає таймаут бездіяльності, після якого спрацює надсилання ReceiveTimeoutmessage. Коли вказане, функцію отримання повинна бути в змозі обробити akka.actor.ReceiveTimeoutmessage. Одна мілісекунда є мінімально підтримуваним таймаутом.

Будь ласка зауважте, що таймаут отриманняможе спрацювати та поставити в чергу повідомлення ReceiveTimeout прямо після іншого повідомлення в черзі; таким чином, не є гарантованим, що під час прийняття таймауту отримання буде попередньо період простою, як сконфігуровано через цей метод.

Коли встановлений, таймаут отримання має дію (тобто, продовжує постійно спрацьовувати після періоду бездіяльності). Передайте inDuration.Undefined для відключення цієї можливості.

  1. import akka.actor.ReceiveTimeout
  2. import scala.concurrent.duration._
  3. class MyActor extends Actor {
  4. // Щоб задати початкову затримку
  5. context.setReceiveTimeout(30 milliseconds)
  6. def receive = {
  7. case "Hello" =>
  8. // Щоб встановити відповідь на повідомлення
  9. context.setReceiveTimeout(100 milliseconds)
  10. case ReceiveTimeout =>
  11. // Щоб відключити
  12. context.setReceiveTimeout(Duration.Undefined)
  13. throw new RuntimeException("Receive timed out")
  14. }
  15. }

Повідомлення, позначені як NotInfluenceReceiveTimeout, не будуть скидати таймер. Це може бути корисним, коли має бути викликаний ReceiveTimeout з зовнішньої неактивності, але не спричиненої внутрішньою активністю, тобто, від спланованих тікових повідомлень.

Зупинка акторів

Актори зупиняються через виклик метода stop на ActorRefFactory, тобто, через ActorContext або ActorSystem. Типово контекст використовується для зупинки самого актора або його дітей, та системи, для зупинення акторів вищого рівня. Справжнє завершення актора виконується асинхронно, stop може повернутись перед завершенням актора.

  1. class MyActor extends Actor {
  2.  
  3. val child: ActorRef = ???
  4.  
  5. def receive = {
  6. case "interrupt-child" =>
  7. context stop child
  8.  
  9. case "done" =>
  10. context stop self
  11. }
  12.  
  13. }

Обробка поточного повідомлення, якщо таке є, буде продовжене, до того, як актор зупиниться, але додаткові повідомлення в поштовій скринці не будуть оброблені. По замовчанню ці повідомлення надсилаються до deadLetters в ActorSystem, але це залежить від реалізації поштової скриньки.

Завершення актора виконується в два кроки: спершу актор призупиняє обробку власної скриньки, та надсилає команду запунки всім дітям, потім він продовжує обробку внутрішніх повідомлень завершення від дітей, до останнього, та потім завершує себе (викликаючи postStop, скидаючи скриньку в дамп, публікуючиTerminated на DeathWatch, та сповіщаючи супервізор). Ця процедура переконує, що піддерева системи акторів завершуються у впорядкований спосіб, просуваючи команду завершення до вузлів, та збираючи їх підтвердження до зупиненого супервізора. Якщо один з акторів не відповідає (обробляє повідомлення довгий період часу, та не відповідає на команду зупинки), весь цей процес може застопоритись.

Під час ActorSystem.terminate захисник системи акторів буде зупинений, та зазначений процес буде гарантувати відповідне завершення цілої системи.

Перехоплювач postStop викликається після того, як актор повністю зупинений. Це дозволяє очищення ресурсів:

  1. override def postStop() {
  2. // очищуємо деякі ресурси ...
  3. }

Зауваження

Оскільки зупинка актора є асинхронною, ви не можете безпосередньо використовувати ім'я дитини, що ви щойно зупинили; це призведе до  InvalidActorNameException. Замість цього, watch за актором, та створіть його заміну у відповідь на повідомлення Terminated, що згодом надійде.

PoisonPill

Ви також можете надіслати акторові повідомлення  akka.actor.PoisonPill, що зупинить актора при обробці цього повідомлення. PoisonPill стає в чергу як звичайне повідомленя, та буде оброблене після повідомлень, що вже наявні в черзі поштової скриньки.

М'яка зупинка

gracefulStop є корисним, коли вам треба зачекати завершення, або узгодити послідовне завершення декількох акторів:

  1. import akka.pattern.gracefulStop
  2. import scala.concurrent.Await
  3.  
  4. try {
  5. val stopped: Future[Boolean] = gracefulStop(actorRef, 5 seconds, Manager.Shutdown)
  6. Await.result(stopped, 6 seconds)
  7. // актор буде зупинений
  8. } catch {
  9. // актор не може зупинитись за 5 секунд
  10. case e: akka.pattern.AskTimeoutException =>
  11. }
  1. object Manager {
  2. case object Shutdown
  3. }
  4.  
  5. class Manager extends Actor {
  6. import Manager._
  7. val worker = context.watch(context.actorOf(Props[Cruncher], "worker"))
  8.  
  9. def receive = {
  10. case "job" => worker ! "crunch"
  11. case Shutdown =>
  12. worker ! PoisonPill
  13. context become shuttingDown
  14. }
  15.  
  16. def shuttingDown: Receive = {
  17. case "job" => sender() ! "service unavailable, shutting down"
  18. case Terminated(`worker`) =>
  19. context stop self
  20. }
  21. }

Коли gracefulStop() повертається успішно, буде виконаний перехоплювач актора postStop(): існує межа відбуволось-перед між кінцем postStop(), та поверненням gracefulStop().

В прикладі вище власне повідомлення Manager.Shutdown надіслане до цільового актора, щоб ініціювати процес зупинки актора. Ви можете задіяти для цього PoisonPill, але тоді ви маєте обмежені можливості, щоб виконати взаємодію з іншими акторами перед зупинкою цільового актора. Прості завдання очистки можуть бути оброблені в postStop.

Попередження

Майте на увазі, що коли актор зупиняється, та його ім'я відреєструється, асинхронно відбуваються окремі події від одного до іншого. Таким чином, можливо ви винайдете деяке ім'я все ще задіяним після повернення gracefulStop(). Щоб гарантувати відповідну дереєстрацію, задійте тільки імена від супервізора, що ви контролюєте, та тільки у відповідьна повідомлення Terminated, тобто, не для акторів вищого рівня.

Become/Unbecome

Upgrade

Akka підтримує горячу заміну цилу повідомлень актора (його реалізації) під час виконання: викличте метод context.become з актора. become приймає  PartialFunction[Any, Unit], що реалізує новий обробник повідомлень. Замінений по гарячому код утримується в стеку Stack, що може бути заштовханий та виштовханий.

Попередження

Будь ласка, зауважте, що актор не буде повертатись до оригінальної поведінки, коли він рестартований супервізором.

Щоб підмінити поведінку актора по гарячому з використанням become:

  1. class HotSwapActor extends Actor {
  2. import context._
  3. def angry: Receive = {
  4. case "foo" => sender() ! "I am already angry?"
  5. case "bar" => become(happy)
  6. }
  7.  
  8. def happy: Receive = {
  9. case "bar" => sender() ! "I am already happy :-)"
  10. case "foo" => become(angry)
  11. }
  12.  
  13. def receive = {
  14. case "foo" => become(angry)
  15. case "bar" => become(happy)
  16. }
  17. }

Цей варіант метода become корисний для багатьої різних речей, як реалізація Машини Кінечних Станів (Finite State Machine, FSM, наприклад, Dining Hakkers). Це замінить поточну поведінку (верхівку стеку поведінки), що означає, що ви не використовуєте unbecome, але замість цього кожного разу встановлюється нова поведінка.

Інший спосіб викорстанняbecome не заміщує, але додає нагору до стеку поведінок. В цьому випадку теба бути уважним, щоб переконатись, що кількісь операцій “виштовхувань” (unbecome) співпадає з числом “заштовхувань”, в довгій перспективі. Інакше це спричинить витік пам'яті  (ось чому ця поведінка не стоїть по замовчанню).

  1. case object Swap
  2. class Swapper extends Actor {
  3. import context._
  4. val log = Logging(system, this)
  5.  
  6. def receive = {
  7. case Swap =>
  8. log.info("Hi")
  9. become({
  10. case Swap =>
  11. log.info("Ho")
  12. unbecome() // скидає найбільший 'become' (просто для втіхи)
  13. }, discardOld = false) // заштовхує нагору, замість заміни
  14. }
  15. }
  16.  
  17. object SwapperApp extends App {
  18. val system = ActorSystem("SwapperSystem")
  19. val swap = system.actorOf(Props[Swapper], name = "swapper")
  20. swap ! Swap // пише Hi
  21. swap ! Swap // пише Ho
  22. swap ! Swap // пише Hi
  23. swap ! Swap // пише Ho
  24. swap ! Swap // пише Hi
  25. swap ! Swap // пише Ho
  26. }

Кодування вкладених отримувань акторів Scala без витоків пам'яті

Дивіться цей приклад отримання невкладених.

Stash

Трейт сховка Stash дозволяє актору тимчасово зберігати повідомлення, що не можуть, або не повинні бути оброблені з використання поточної поведінки актора. Перед зміню обробника повідомлень актора, тобто, прячмо перед виикликом context.become orcontext.unbecome, всі приховані повідомлення можуть бути "відзбережені", так що вони опиняться на початку поштової скриньки актора. Таким чином, приховані повідомлення можуть бути оброблені в тому ж порядку, що вони були отримані.

Зауваження

Трейт Stash розширює маркер-трейт  RequiresMessageQueue[DequeBasedMessageQueueSemantics], що опитує систему, щоб автоматично обирати видбір з черги на основі реалізації для актора. Якщо ви бажаєте більше керування над поштовою скринькою, дивіться документацію щодо скриньок: Поштові скриньки.

Ось приклад Stash в дії:

  1. import akka.actor.Stash
  2. class ActorWithProtocol extends Actor with Stash {
  3. def receive = {
  4. case "open" =>
  5. unstashAll()
  6. context.become({
  7. case "write" => // записуємо...
  8. case "close" =>
  9. unstashAll()
  10. context.unbecome()
  11. case msg => stash()
  12. }, discardOld = false) // стек нагору замість заміщення
  13. case msg => stash()
  14. }
  15. }

Виклик stash() додає поточне повідомлення (повідомлення, що актор отримав останнім) до сховку актора. Це типово викликається, коли обробляється випадо по замовчанню, щоб зберігти повідомлення, що не оброблене в інших випадках. Є нелегальним приховувати те ж повідомлення двічі; це призведе до виклику IllegalStateException. Сховок також може бути одмежений, та в цьому випадку виклик stash() може призвести до порушення місткості, та згодом до StashOverflowException. Місткість сховку може бути сконфігурована через stash-capacitysetting (Int) в конфігурації поштової скриньки.

Виклик unstashAll() відкликає повідомлення зі сховку до скриньки актора, доки не буде досягнута, якщо є, місткість поштової скриньки (зауважте, що повідомлення зі сховка стають наперед скриньки). В випадку  переповнення обмеженої скриньки, підійметьсяMessageQueueAppendFailedException. Сховок буде гарантовано порожній після викликуunstashAll().

Сховок підтримується через scala.collection.immutable.Vector. Як слідоцтво, навіть кожне велике число повідомлень може бути приховане без значного впливу на продуктивність.

Попередження

Зауважте, що трейт Stash має бути зміксованй до (або до субкласу) терйту Actor, перед тим, як любий трейт або клас перекриє зворотній виклик preRestart. Це означає, що не можливо написати Actor with MyActor with Stash, якщо MyActor перекриває preRestart.

Зауважте, що сховок є частиною ефемерного стану актора, на відміну від поштової скриньки. Таким чином, він має керуватись як інші частини стану актора, що мають ті ж властивості. Реалізація трейту Stash в preRestart буде викликати unstashAll(), що звичайно є бажаною поведінкою.

Зауваження

Якщо ви бажаєте примусити, щоб ваш актор міг робити тільки з необмеженим сховком, вам треба замість цього скористатитсь трейтом UnboundedStash.

Вбивство актора

Ви можете вбити актора, надсилаючи повідомлення Kill. Це спричинить виклик акторомActorKilledException, що дасть збій. Актор призупинить обробку, та його супервізор отримає запит, як обробити цю відмову, що може означати відновлення актора, його рестарт, або його повне завершення. Дивіться Що означає нагляд супервізора для додаткової інформації.

Використовуйте Kill таким чином:

  1. // вбити актора 'victim'
  2. victim ! Kill

Актори та виключення

Може трапитись, що під час обробки повідомлення актором, виникне деякий різновид виключення, як виключення бази даних.

Що відбудеться з повідомленням

Якщо відбудеться виключення під час обробки повідомлення (тобто воно вибране зі скриньки, та оброблене поточною поведінкою), тоді повідомлення буде втрачене. Важливо розуміти, що воно не повернеться до поштової скриньки. Так що якщо ви бажаєте повторити обробку повідомлення, вам треба мати справу з цім самотужки, перехоплюючи виключення, та повторюючи все з початку. Будьте впевнені, що ви наклали обмеження на число повторів, оскільки ви не бажаєте загнати систему в постійний цикл (що споживає цикли процесора без жодного прогресу). Інша можливість може полягати в розгляданні шаблону PeekMailbox.

Що відбувається з поштовою скринькою

Якщо під час обробки повідомлення відбувається виключення, з поштовою скринькою нічого не відбувається. Якщо актор рестартує, він отримає ту ж скриньку. Так що всі повідомлення також будуть на місці.

Щоб відбувається з актором

Якщо код актора викликає виключення, цей актор призупиняється, та стартує процес супервізора(дивіться Супервізор та моніторинг). В залежності від рішення супервізора, актор відновлюється (якби нічого не трапилось), рестартує (очищуючи його внутрішній стан, та починаючи з нуля), або завершується.

Розширення акторів за допомогою зчеплення PartialFunction

Іноді може бути корисним розділяти загальну поведінку між декількома акторами, або складати поведінку одного актора з декількох меньших функцій. Це можливо, оскільки метод актора receive повертає Actor.Receive, що є псевдонімом типу для PartialFunction[Any,Unit], та часткові функції можуть бути зчеплені разом, з використанням метода  PartialFunction#orElse. Ви можете зціпити стільки функцій, скільки треба, але ви не повинні забувати, що перемагає "перше співпадіння" - що може бути важливо, коли комбінуєте функції, що обоє можуть обробляти той самий тип повідомлень.

Наприклад, уявімо, що ми маємо набір акторів, що є або Producers, або Consumers,  при чому іноді має сенс мати актора, що розділяє обоє поведінки. Це можна просто досягти без дублкації кода, виділяючи поведінки у трейти, та реалізуючи receive акторів як комбінацію ціх часткових функцій.

  1. trait ProducerBehavior {
  2. this: Actor =>
  3.  
  4. val producerBehavior: Receive = {
  5. case GiveMeThings =>
  6. sender() ! Give("thing")
  7. }
  8. }
  9.  
  10. trait ConsumerBehavior {
  11. this: Actor with ActorLogging =>
  12.  
  13. val consumerBehavior: Receive = {
  14. case ref: ActorRef =>
  15. ref ! GiveMeThings
  16.  
  17. case Give(thing) =>
  18. log.info("Got a thing! It's {}", thing)
  19. }
  20. }
  21.  
  22. class Producer extends Actor with ProducerBehavior {
  23. def receive = producerBehavior
  24. }
  25.  
  26. class Consumer extends Actor with ActorLogging with ConsumerBehavior {
  27. def receive = consumerBehavior
  28. }
  29.  
  30. class ProducerConsumer extends Actor with ActorLogging
  31. with ProducerBehavior with ConsumerBehavior {
  32.  
  33. def receive = producerBehavior.orElse[Any, Unit](consumerBehavior)
  34. }
  35.  
  36. // протокол
  37. case object GiveMeThings
  38. final case class Give(thing: Any)

Замість наслідування, той же шаблон може бути застосований через композицію  - дехто може просто скомпонувати метод receive, з використанням часткові функції від делегатів.

Шаблони ініціалізації

Багаті перехоплювачі життєвого циклу акторів провадять корисний набір інструментів для реалізації різних шаблонів ініціалізації. На протязі життєвого циклу ActorRef, актор може потенційно пройти через декілька рестартів, коли старий примірник замінюється на новий, непомітно для зовнішнього спостерігача, що бачить тільки ActorRef.

Дехто може думати про новий примірник, як про "інкарнації". Ініціалізація може бути необхідною для кожної інкарнації актора, але іноді дехто потребуватиме, щоб ініціалізація відбувалась тільки ри народженні першого примірника, коли створюється ActorRef. Наступні розділи провадять шаблони для різних потреб ініціалізації.

Ініціалізація через конструктор

Використання конструктора для ініціалізації має різні вигоди. Зпершу, це робить можливим використання полів val для зберігання любого стану, що не змінюється на протязі життя примірника актора більш міцним. Конструктор викликається за кожної інкарнації актора, таким чином вміст актора може завжди припускати, що відбулась відповідна ініціалізація. Це також є недоліком цього підходу, оскільки є випадки, коли дехто бажає уникнути повторної ініціалізації вмісту під час рестарту. Наприклад, це часто є корисним для зберігання дитячих акторів між рестартами. Наступний розділ провадить шаблон для цього випадку.

Ініціалізація через preStart

Метод актора preStart() напряму викликається тільки один раз, під час ініціалізації першого примірника, тобто, при створенні його ActorRef. В випадку рестартів, preStart() викликається з  postRestart(), і таким чином, якщо не перекритий, preStart() викликається для кожної інкарнації. Однак перекриваючи postRestart() ви можете відключити цю поведінку, та забезпечити, що відбувається тільки один виклик до preStart().

Одне корисне використання цього шаблону є відключення створення нових ActorRefs для дітей під час рестартів. Це може бути досягнуте перекриттям preRestart():

  1. override def preStart(): Unit = {
  2. // Ініціалізуйте дітей тут
  3. }
  4.  
  5. // Перекриття postRestart щоб відключити виклик до preStart()
  6. // після рестартів
  7. override def postRestart(reason: Throwable): Unit = ()
  8.  
  9. // Реалізація по замовчанню preRestart() зупиняє всіх дітей
  10. // актора. Щоб відмовитись від зупинки дітей, ми маємо
  11. // перекрити preRestart()
  12. override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
  13. // Зберігайте виклик postStop(), але не зупуняємо дітей
  14. postStop()
  15. }

Будь ласка зауважте, що дитячі актори все ще рестартують, але без створення нового ActorRef. Дехто може застосувати рекурсивно ті ж принципи для дітей, гарантуючи, що їх метод preStart() викликається тільки при створенні їх посилань.

For more information see What Restarting Means.

Ініціалізація через надсилання повідомлення

Є випадки, коли неможливо передати потрібну для ініціалізації інформацію в конструктор, наприклад, в присутності кругових залежностей. В цьому випадку актор повинен очікувати повідомлення ініціалізації, та використовуйте become(), або перехід від кінечних до машинних станів, щоб закодувати ініціалізовані та неініціалізовані стані актора.

  1. var initializeMe: Option[String] = None
  2.  
  3. override def receive = {
  4. case "init" =>
  5. initializeMe = Some("Up and running")
  6. context.become(initialized, discardOld = true)
  7.  
  8. }
  9.  
  10. def initialized: Receive = {
  11. case "U OK?" => initializeMe foreach { sender() ! _ }
  12. }

Якщо актор може отримувати повідомлення перед тим, як він буде ініціалізований, корисни мінструментом може виявитись Stash для збереження повідомлень, доки не завершиться ініціалізація, та програвання їх після того, як автор буде ініціалізований.

Попередження

Цей шаблон має використовуватись з обережністю, та застосовується  тільки коли жодний з шаблонів вище не може бути застосований. Однією з потенційних проблем може бути втрата повідомлень при надсиланні до віддалених акторів. Також публікація ActorRef в неініціалізованому стані актора може призвести до умов, коли він отримує користувацькі повідомлення перед тим, як буде виконана ініціалізація.

Akka Typed

Попередження

Цей модуль наразі є експериментальним, в тому сенсі, що є предметом активної розробки. Це означає, що API або семантика може змінитись без попередження або періода амортизації, тому не рекомендано використовувати цей модуль в виробництві прямо зараз  — вас попередили.

Як дискутовано в Системах акторів (та наступних главах) актори існують заради надсилання повідомлень між незалежними одиницями обчислень, але як це виглядає? Все подальше очікує, що імпортовані наступні бібліотеки:

  1. import akka.typed._
  2. import akka.typed.ScalaDSL._
  3. import akka.typed.AskPattern._
  4. import scala.concurrent.Future
  5. import scala.concurrent.duration._
  6. import scala.concurrent.Await

Коли це на місці, ми можемо визначити нашого першого актора, та, звичайно, ми скажемо привіт!

  1. object HelloWorld {
  2. final case class Greet(whom: String, replyTo: ActorRef[Greeted])
  3. final case class Greeted(whom: String)
  4.  
  5. val greeter = Static[Greet] { msg =>
  6. println(s"Hello ${msg.whom}!")
  7. msg.replyTo ! Greeted(msg.whom)
  8. }
  9. }

Цей малий шматок кода визначає два типи повідомлень, один що наказує актору привітати декого, та другий актор буде використовуватись для підтвердження, що це зроблене. Тип Greet містить не тільки інформацію про те, кого привітати, він також містить ActorRef, що постачає надсилач повідомлення, так що актор HelloWorld може надіслати назад повідомлення з підтвердженням.

Поведінка актора визначена як значення greeter за допомогою конструктора поведінки Static — є багато різних шляхів зформулювати поведінки, як ми побачимо в подальшому. “Статична” поведінка не здатна вмінюватись у відповідь на повідомлення, вона залишатиметься такою ж, доки актор не буде зупинений батьком.

Тип повідомлення, що обробляється цією поведінкою, декларований як клас Greet, що має на увазі, що аргумент наданої функції msg також такого типу. Ось чому ми можемо отримати доступ до членів whom та replyTo, без потреби використовувати порівняння з шаблоном.

В останньому рядку ми бачимо, як актор HelloWorld надсилає повідомлення іншому акторові, що робиться за допомогою оператора !  (промовляється “tell”). Оскільки адреса replyTo декларована як тип  ActorRef[Greeted], компілятор буде дозволяти нам надсилать повідомлення тільки цього типу, інше використання не буде сприятись.

Прийнятні типи повідомлень актора, разом зі всіма типами відповідей визначають протокол, яким розмовляє цей актор; в цьому випадку це простий протокол запиту-відповіді, але актори в разі потреби можуть моделювати довільно складні протоколи. Протокол запакований разом з поведінкою, що реалізує його в гарно огорнутому полі зору  — об'єкті HelloWorld.

Тепер ми бажаємо випробувати цього актора, так що ми маємо запустити систему акторів, де він розміститься:

  1. import HelloWorld._
  2. // використовуємо глобальний пул, бо ми бажаємо виконувати речі післі system.terminate
  3. import scala.concurrent.ExecutionContext.Implicits.global
  4.  
  5. val system: ActorSystem[Greet] = ActorSystem("hello", Props(greeter))
  6.  
  7. val future: Future[Greeted] = system ? (Greet("world", _))
  8.  
  9. for {
  10. greeting <- future.recover { case ex => ex.getMessage }
  11. done <- { println(s"result: $greeting"); system.terminate() }
  12. } println("system terminated")

Після імпорту визначення протокола актора, ми запускаємо систему акторів з визначеною поведінкою, огортаючи її в Props, як актора на сцені. Властивості props, що ми передаємо до цієї системи, є тільки замовченнями. Ми можемо в цій точці також сконфігурувати, як та де актор повинен бути розгорнений в кластерній системі.

Як каже Карл Хьювіт, один актор ще не актор — він буде досить самотнім, коли ні з ким поговорити. В сенсі нашого приклада, це трохи жорстко, оскільки ми надали актору тільки HelloWorld підробну особу для ромов - шаблон “ask” (представлений оператором ? ), що може бути використаний для надсилання повідомлень, так, щоб відповідь відповідала Promise, до якого ми повертаємо відповідне Future.

Зауважте, що це Future, що повертається операцією “ask”, вже відповідно типізоване, перевірка типу або приведення не потрібні. Це можливо через інформацію типу, що є частиною протокола повідомлень: оператор ? приймає як аргумент функцію, що приймає ActorRef[U] (що пояснює дірку  _ в виразі на рядку 6 вище), та параметр replyTo, що ми заповнюємо як такий, що має тип ActorRef[Greeted], що означає, що значення, що задовільняє Promise, може бути тільки типу Greeted.

Ми використовуємо це тут для надсилання команди Greet актору, та потім для відповідь надходить назад, ми її роздруковуємо, та наказуємо системі акторів зупинитись. Коли це також зроблене, ми друкуємо повідомлення "system terminated", та програма завершується. Комбінатор recovery на оригінальному Future потрібен, щоб переконатись в відповідньому завершенні системи, навіть якщо щось піде не так; комбінатори flatMap та map для вираза for починають турбуватись тільки щодо  “щасливого шляху”, та якщо future схибить через таймаут, тоді greeting не буде виділений, та нічого взагалі не трапиться.

Це показує, що є аспекти повідомлень акторів, що можуть бути перевірені на тип компілятором, але ця можливість не безмежна, є обмеження того, що ми можемо виразити статично. Перед тим, як ми перейдемо до більш складного  (та реалістичного) приклада, ми зробимо малий відступ, щоб висвітлити деяку теорію позаду всього цього.

Крихта теорії

Модель акторів, як визначено Хьювітом, Бішопом та Стайгером в 1973му році, є обчислювальною моделлю, що виражає саме те, що це означає бути розподіленим для обчислень. Одиниці обробки, актори, можуть комунікувати тільки через обмін повідомленнями,  та при прийомі повідомлення актор може виконати три наступні фундаментальні дії:

  1. надіслати скінчене число повідомлень акторам, яких він знає
  2. створити скінчене число нових акторів
  3. визначити поведінку, що буде застосована до наступного повідомлення

Проект Akka Typed виражає ці дії з використанням поведінок та адрес. Повідомлення можуть бути надіслані за адресами, та за цім фасадом існує поведінка, що отримує повідомлення, та діє на його основі. Зв'язування між адресами та поведінкою може змінюватись з часом, відповідно до третього пункту вище, але це не видиме ззовні.

З такою преамбулою ми можемо перейти до унікальної властивості цього проекту, точніше, саме те, що від вводить статичну перевірку взаємодії акторів: адреси параметризовані, та ім можуть надсилатись тільки повідомлення, що мають певний тип. Асоціація між адресами та їх параметром типа, мусить бути зроблена, коли створюються адреса (та її актор). З цією ціллю кожна поведінка також параметризована типом повідомлень, що вона може обробляти. Оскільки поведінка може змінюватись за фасадом адреси, визначення наступної поведінки є обмеженою операцією: послідовник мусить обробляти той же тип повідомлень, що і попередник. Це необхідно, щоб не зруйнувати адреси, що посилаються на актора.

Що це дає, це коли повідомлення надсилається до актора, де ми можемо статично переконатись, що тип повідомлення одне з тих, що актор декларує як оброблювані — ми можемо уникнути помилки надсилання повністю безглуздих повідомлень. Однак в чому ми не можемо перконатись, це в тому, що поведінка поза адерсою буде в певносу стані, коли повідомлення буде отриманим. Фундаментальна причина є в тому, що  асоціація між адресою та поведінкою є динамічною властивістю часу виконання, та компілятор не може знати цього, доки він не транслює початковий код.

Це те ж саме, що і для нормальних об'єктів Java з внутрішніми змінними: при компіляції програми ми не можемо знати, якими будуть їх значення, та чи результат виклику метода залежить від тих змінних, бо результат непевний до певної міри —ми можемо тільки бути певними, що повернуте значення є даного типа.

Ми бачили вище, що тип результату команди актора описаний типом адереси reply-to, тобто, міститься в повідомленні. Це дозволяє, щоб перетворення було описане в термінах типів: відповідь буде типа A, але вона може також містити адресу типу B, що потім дозволяє іншому актору продовжити розмову, надсилаючи повідомлення типу B цьому новому акторові.  Хоча ми не можемо статично виразити “поточний” стан актора, ми можемо виразити поточний стан протокола між двома акторами, оскільки він надається типом останнього повідомлення, що було отримане або надіслане.

В наступному розділі ми продемонструємо це на більш реалістичному прикладі.

Більш складний приклад

Розглянемо актора, що керує кімнатою чату: клієнтські актори можуть з'єднатись, надсилаючи повідомлення,  що містить їх екранне ім'я, та після цього вони можуть надсилати повідомлення. Актор чат кімнати буде розбирати всі надіслані повідомлення до всіх наразі під'єднаних клієнтськіх акторів. Визначення протокола може виглядати таким чином:

  1. sealed trait Command
  2. final case class GetSession(screenName: String, replyTo: ActorRef[SessionEvent])
  3. extends Command
  4.  
  5. sealed trait SessionEvent
  6. final case class SessionGranted(handle: ActorRef[PostMessage]) extends SessionEvent
  7. final case class SessionDenied(reason: String) extends SessionEvent
  8. final case class MessagePosted(screenName: String, message: String) extends SessionEvent
  9.  
  10. final case class PostMessage(message: String)

Зпочатку клієнтській актор отримуює доступ тільки до ActorRef[GetSession], що дозволяє їм зробити перший крок. Коли клієнтська сессія встановлена, вit отримує повідомлення SessionGranted, що містить handle, щоб розблокувати наступний крок протокола, публікацію повідомлень. Команда PostMessage буде надсилатись на цю окрему адресу, що представляє сесію, якак додається до кімнати чата. Інший аспект сесії в тому, що клієнт розкриває свою власну адресу через аргумент replyTo, так що наступні події MessagePosted можуть надсилатись до нього.

Це ілюструє, як актори можуть виражати більше, ніж тільки еквівалент виклику метода для Java об'єктів. Декларовані типи повідомлення та їх вміст описують повний протокол, що може залучати декіклька акторів, та що може включати декіклька кроків. Реалізація протокола кімнати чата може бути такою простою, як це:

  1. private final case class PostSessionMessage(screenName: String, message: String)
  2. extends Command
  3.  
  4. val behavior: Behavior[GetSession] =
  5. ContextAware[Command] { ctx =>
  6. var sessions = List.empty[ActorRef[SessionEvent]]
  7.  
  8. Static {
  9. case GetSession(screenName, client) =>
  10. sessions ::= client
  11. val wrapper = ctx.spawnAdapter {
  12. p: PostMessage => PostSessionMessage(screenName, p.message)
  13. }
  14. client ! SessionGranted(wrapper)
  15. case PostSessionMessage(screenName, message) =>
  16. val mp = MessagePosted(screenName, message)
  17. sessions foreach (_ ! mp)
  18. }
  19. }.narrow // тільки показує GetSession назовні

Ядро цієї поведінки знов статичне, сама кімната чату не змінюється в щось інше, коли встановлюються сесії, але ми вводимо змінну, що відслідковує відкриті сесії. Коли надходить нова команда GetSession, ми додаємо цього клієнта до списку, та потім нам треба створити ActorRef сесії, що буде використане для публікації повідомлень. В цьому випадку ми бажаємо створити дуже простого актора, що просто перепаковує команду PostMessage на команду  PostSessionMessage, що також включає екранне ім'я. Такий актор-огортка може бути створена з використанням метода spawnAdapter на ActorContext, так що  потім ми можемо перейти до відповіді клієнту з результатом SessionGranted.

Поведінка, що ми тут декларуємо, може обробляти обоє підтипи Command. GetSession вже був пояснений, та команди PostSessionMessage, що походять від акторів-огорток, будуть перемикати  поширення повідомлень чат кімнати на всіх під'єднаних клієнтів. Але ми не бажаємо надати можливість надсилати командиPostSessionMessage до довільних клієнтів, ми резервуємо це право для оболонок, що ми створюємо — інакше клієнти зможуть видавати себе за повністі інші екранні імена (уявіть протокол GetSession, що включає інформацію аутентифікації, що ще унебезпечує це). Таким чином, ми обмежуємо поведінку до тільки прийняття команд GetSession,  перед тим, як представити їх світу, і, таким чином, тип значення behavior є Behavior[GetSession], замість Behavior[Command].

Звуження типу поведінки завжди безпечна операція, оскільки вона тільки обмежує, що може робити клієнт. Якщо ми розширимо тип, тоді клієнти можуть надсилати інші повідомлення, що не очікувались при написанні первинного кода для поведінки.

Якщо ми не стурбовані безпекою відповідності між сесією та екранним ім'ям, ми можемо змінити протокол, видаливши PostMessage, та всі клієнти отримають тількиActorRef[PostSessionMessage] куди треба надсилати. В цьому випадку не потрібна огортка, та ми можемо просто використовувати ctx.self. Перевірки типу відробляють в цьому віпадку, оскільки ActorRef[-T] є контрваріантним в його параметрі типа, що означає, що ми можемо використосувати ActorRef[Command]кожного разу, коли потрібен  ActorRef[PostSessionMessage] — це має сенс, оскільки попередній просто розмовляє більшими мовами, ніж останній.  Навпаки буде проблематичним, так що передача ActorRef[PostSessionMessage] де потрібнеActorRef[Command] призведе до помилки типа.

Останній шматок цього визначення поведінки є декоратор ContextAware, що ми використовуємо, щоб отримати доступ до ActorContext в Static визначенні поведінки. Цей декоратор викликає запроважену функцію , коли отримане перше повідомлення, та, таким чином. створює реальну поведінку, що буде використовуватись в подальшому — декоратор відкидається, після того, як зробив свою роботу.

Спробуємо це

Щоб побачити цю чат кімнату в дії нам треба написати клієнта-актора, що буде використовувати її:

  1. import ChatRoom._
  2.  
  3. val gabbler: Behavior[SessionEvent] =
  4. Total {
  5. case SessionDenied(reason) =>
  6. println(s"cannot start chat room session: $reason")
  7. Stopped
  8. case SessionGranted(handle) =>
  9. handle ! PostMessage("Hello World!")
  10. Same
  11. case MessagePosted(screenName, message) =>
  12. println(s"message has been posted by '$screenName': $message")
  13. Stopped
  14. }

З цієї поведінки ми можемо створити актора, що буде приймати сесію чат кімнати, надсилати повідомлення, очікувати, щоб побачити що воно обубліковане, та потім завершуватись. Останній крок потребує можливості змінити поведінку, нам треба перейти від нормальної робочої поведінки до стану завершення. Ось чому актор використовує конструктор різних поведінок з назвою Total. Цей конструктор отримує як аргумент функцію від обробленого типу повідомлення, в цьому випадку SessionEvent, до наступної поведінки. Ця наступна поведінка мусить знову бути того ж типа, як ми обсудили в теоретичному розділі вище. Тут ми або залишаємось в тій самій поведінці, або ми завершуємось, та обоє ці випадки є такими загальними, що є спеціальні значення Same та Stopped, що ми можемо використовувати. Поведінка названа “total” (на відміну від “partial”), оскільки задекларована функція мусить обробляти всі значення її вхідного типа. Оскільки SessionEvent є зв'язаним трейтом, компілятор Scala буде попереджати, якщо ми забудемо обробити один з його підтипів; в цьому випадку він нагадає нам, що альтернативно до SessionGranted ми можето також отримати подію SessionDenied.

Тепер, щоб спробувати це, ми маємо запустити обоє, чат кімнату та gabbler, та, звичайно, ми робимо це в системі акторів. Оскільки може бути тільки один охоронець-супервізор, ми можемо або стартувати чат з  gabbler (що для нас небажано — це ускладнює його логіку), або gabbler з чат кімнати (що безглуздо), або ми стартуємо обоє з третього актора — наш єдиний логічний вибір:

  1. val main: Behavior[Unit] =
  2. Full {
  3. case Sig(ctx, PreStart) =>
  4. val chatRoom = ctx.spawn(Props(ChatRoom.behavior), "chatroom")
  5. val gabblerRef = ctx.spawn(Props(gabbler), "gabbler")
  6. ctx.watch(gabblerRef)
  7. chatRoom ! GetSession("ol’ Gabbler", gabblerRef)
  8. Same
  9. case Sig(_, Terminated(ref)) =>
  10. Stopped
  11. }
  12.  
  13. val system = ActorSystem("ChatRoomDemo", Props(main))
  14. Await.result(system.whenTerminated, 1.second)

В гарних традиціях, ми викликаємо актора main, чим він і є, напряму відповідаючи методу main в традиційних застосуваннях Java. Цей актор буде виконувати свою роботу за його власним бажанням, нам не треба надсилати повідомлення ззовні, так що ми вирішуємо, щоб він був типа Unit. Актори отримують не тільки зовнішні повідомлення, вони також повідомляються по певні системні події, так звані сигнали. Щоб отримати доступ до них, ми обираємо реалізувати цього окремого актора з використанням декоратора поведінки  Full. Ім'я  походить від факта, що  в ньому ми маємо повний доступ до всіх аспектів актора. Запроваджена функція буде викликана для сигналів  (огорнутих в  Sig) або користувацьких повідомлень (огорнутих в  Msg), та огортка також містить посилання на ActorContext.

Цей окремий актор main реагує на два сигнали: коли він запускаєтьяс, він спочатку отримає сигнал PreStart, під час чого створюються чат кімната та gabbler, та ініціюється сесія між ними, та коли gabbler завершується, ми отримаємо подію Terminated, тому що викликали ctx.watch для нього. Це дозволяє нам завершити систему акторів: коли актор main завершується, більше нічого робити.

Таким чином, після створення системи акторів з Props актора main, нам залишається тільки очікувати на його завершення.

Статус цього проекту та відношення до Akka Actors

Akka Typed є результатом багатьох років досліджень та спроб (включаючи Typed Channels в серії 2.2.x), та він на шляху до стабілізації, але дозрівання такої глибинної зміни до ядра концепції Akka займе довгий час. Ми очікуємо, що цей моудль буде залишатись експериментальним для декільког головних релізів Akka, та звичайний akka.actor.Actor не буде амартизований або піде зі сцени в найближчому часі.

Факт, що це дослідницькій проект, також тягне за собою факт, що документація посилання не така деталізована, якою вона буде для фінальної версії, посилайтесь до документації API щодо більш глибоких та точніших деталей.

Головні відмінності

Найбільш яскрава відмінність полягає в видаленні функціональності sender(). Це виявилося ахилесовою п'ятою проекта Typed Channels, це можливість, що робить її сигнатури типів та такорси дуже складними, щоб вони були життєспроможніми. Рішення, прийняте в Akka Typed є явно включити відповідно типізовану адресу reply-to в повідомлення, що обоє,  обтяжує користувача цім завданням, але аткож покладає новий аспект розробки протокола, до якого нележить.

Інша яскрава відмінність є видалення трейта Actor. Щоб уникнути замикання над нестабільними посиланнями від різних контекстів виконання (наприклад, трансформацій Future), ми перетворили всі залишкі методів, що були на цьому трейті, на повідомлення: поведінка отримує ActorContext як аргумент під час обробки, та перехоплення життєвого циклу перетворились на Signal.

Побічний ефект цього в тому, що поведінки тепер можуть бути протестовані в ізоляції, без того, щоб пакуватись в актора, тести можуть робити повністю синхронно, не турбуючись щодо таймаутів та  підробних збоїв. Інший побічний ефект в тому, що поведінки можуть гарно бути скомпоновані та декоровані, дивіться комбінатори And, Or, Widened, ContextAware; тут немає нічного особливого або поємного, нові комбінатори можуть бути написані як звонішні бібліотеки, або закроєні під кожний проект.

Стійкість до відмов

Як пояснювалось в Системах акторів, кожний актор є супервізором для дітей, та як такий кожний актор визначає стратегію обробки відмов супервізора. Ця стратегія не може бути змінена після цього, бо є невід'ємною частиною структури системи акторів.

Обробка відмов на практиці

Перше, давайте поглянемо на приклад, що ілюструє один зі шляхів до обробки помилок сховища даних, що є типовим джерелом відмов в застосуваннях реального світу. Звичайно, це залежить від конкретного застосування, що можна зробити, коли сховище даних недосяжне, але в цьому прикладі ми використовуємо кращі намагання в підході пере-з'єднання.

Прочитайте наступний код. Коментарі в тексті пояснюють різні частини обробки збоїв, та чому вони додані. Також дуже рекомендоано виконати цей приклад, тому що просто слідувати виводу журнала, щоб зрозуміти, що відбувається під час виконання.

Створення стратегії супервізора

Наступні розділи більш заглиблено пояснюють механізм обробки відмов, та альтернативи.

В цілях демонстрації давайте розглянемо наступну стратегію:

  1. import akka.actor.OneForOneStrategy
  2. import akka.actor.SupervisorStrategy._
  3. import scala.concurrent.duration._
  4.  
  5. override val supervisorStrategy =
  6. OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) {
  7. case _: ArithmeticException => Resume
  8. case _: NullPointerException => Restart
  9. case _: IllegalArgumentException => Stop
  10. case _: Exception => Escalate
  11. }

Я обрав декілька гарно відомих типів виключень, щоб продемострувати застосування директив обробки збоїв, описаних в Супервізор та мониторинг. Для початку, це стратегія кожний-за-себе (one-for-one), цо означає, що кожна дитина трактується окремо (стратегія всі-за-одного (all-for-one) робить дуже подібно, з однією різницею, що кожне рішення стосується до всіх дітей супервізора, те тільки до того, хто схибив). Існуює набір обмежень щодо частоти рестартів, наразі 10 рестартів на хвилину; кожне з ціх налаштувань можна опустити, що означає, що відповідний ліміт не застосовується, що лишає можливість вказати абсолютний верхній ліміт на рестарти, або щоб рестарти робили без обмежень. Якщо досягнуто ліміта, дитячий актор зупиняється.

Твердження match, що формує основу тіла, є типу Decider, що те саме, що PartialFunction[Throwable,Directive]. Цей шматок відображує типи відмов дітей на відповідні директиви.

Зауваження

Якщо стратегія декларована в акторі супервізора  (на відміну від об'єкта-компан'она), його вирішувач має доступ до всього внутрішнього стана актора в потоко-безпечний спосіб, включаючи отримання посилання на тільки що схибивше дитя (доступно як sender повідомлення про відмову).

Стратегія супервізора по замовчанню

Escalate використовується, якщо визначена стратегія не охоплює виключення, що будо підійняте.

Коли стратегія супервізора не визначена для актора, наступні виключення обробляються по замовчанню:

  • ActorInitializationException зупиняє викликавшу дитину-актора
  • ActorKilledException зупиняє викликавшу дитину-актора
  • Exception рестартує викликавшу дитину-актора
  • Інші типи Throwable будуть передані до батьківського актора

Якщо виключення ескалується весь час, до самого кореневого охоронця, він буде обробляти його в той же спосіб, що і стратегія по замовчанню, визначена вище.

Ви можете комбінувати вашу власну стратегію зі стратегією по замовчанню:

  1. import akka.actor.OneForOneStrategy
  2. import akka.actor.SupervisorStrategy._
  3. import scala.concurrent.duration._
  4.  
  5. override val supervisorStrategy =
  6. OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) {
  7. case _: ArithmeticException => Resume
  8. case t =>
  9. super.supervisorStrategy.decider.applyOrElse(t, (_: Any) => Escalate)
  10. }

Зупинка стратегії супервізора

Ближчий до Erlang спосіб є стратегія просто зупиняти дітей, коли вони схибили, та потім приймати дії з корегування в супервізорі, коли DeathWatch сигналізує про втрату дитини. Ця стратегія також провадиться з коробки як SupervisorStrategy.stoppingStrategy у супровіді з конфігуратором  StoppingSupervisorStrategy, що буде використовуватись, коли ви бажаєте, щоб захисник   "/user" застосував її.

Журналювання збоїв акторів

По замовчанню SupervisorStrategy журналює збої, тільки якщо вони не еккалюють. Ескальовані збої розглядаються як оброблені, та потенційно журнальовані, на вищому рівні ієрархії.

Ви можете змінити журналювання по замовчанню SupervisorStrategy, встановивши  loggingEnabled в false при створенні примірника. Власне журналювання може бути зроблене всередині Decider. Зауважте, що посилання на поточне збійне дитя доступне як sender, колиSupervisorStrategy задеклароване всередині актора наглядача.

Ви можете також налаштувати журналювання в вашій власній реалізації  SupervisorStrategy, перевизначивши методlogFailure.

Нагляд за акторами вищого рівня

Актори вищого рівня означають ті, що створені з використанням system.actorOf(), та вони діти Користувацького захисника. Немає особливих правил, що стосуються цього випадка, захисник просто застосовує сконфігуровану стратегію.

Тестове застосування

Наступний розділ показує ефекти різних директив на практиці, там, де треба тестове налаштування. Для початку, нам треба підходящий супервізор:

  1. import akka.actor.Actor
  2.  
  3. class Supervisor extends Actor {
  4. import akka.actor.OneForOneStrategy
  5. import akka.actor.SupervisorStrategy._
  6. import scala.concurrent.duration._
  7.  
  8. override val supervisorStrategy =
  9. OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) {
  10. case _: ArithmeticException => Resume
  11. case _: NullPointerException => Restart
  12. case _: IllegalArgumentException => Stop
  13. case _: Exception => Escalate
  14. }
  15.  
  16. def receive = {
  17. case p: Props => sender() ! context.actorOf(p)
  18. }
  19. }

Цей супервізор буде використаний для створення дітей, з якими ми можемо експериментувати:

  1. import akka.actor.Actor
  2.  
  3. class Child extends Actor {
  4. var state = 0
  5. def receive = {
  6. case ex: Exception => throw ex
  7. case x: Int => state = x
  8. case "get" => sender() ! state
  9. }
  10. }

Тест простіший з використанням утіліт, описаних в  Тестуванні систем акторів.

  1. import com.typesafe.config.{ Config, ConfigFactory }
  2. import org.scalatest.{ FlatSpecLike, Matchers, BeforeAndAfterAll }
  3. import akka.testkit.{ TestActors, TestKit, ImplicitSender, EventFilter }
  4.  
  5. class FaultHandlingDocSpec(_system: ActorSystem) extends TestKit(_system)
  6. with ImplicitSender with FlatSpecLike with Matchers with BeforeAndAfterAll {
  7.  
  8. def this() = this(ActorSystem(
  9. "FaultHandlingDocSpec",
  10. ConfigFactory.parseString("""
  11. akka {
  12. loggers = ["akka.testkit.TestEventListener"]
  13. loglevel = "WARNING"
  14. }
  15. """)))
  16.  
  17. override def afterAll {
  18. TestKit.shutdownActorSystem(system)
  19. }
  20.  
  21. "A supervisor" must "apply the chosen strategy for its child" in {
  22. // тут код
  23. }
  24. }

Давайте створимо акторів:

  1. val supervisor = system.actorOf(Props[Supervisor], "supervisor")
  2.  
  3. supervisor ! Props[Child]
  4. val child = expectMsgType[ActorRef] // отирмати відповідь від TestKit testActor

Перший тест повинен демонструвати директиву Resume, так що ми спробуємо це, через задання деякого не-первинного стану в акторі, та змусимо його схибити:

  1. child ! 42 // встановити стан в 42
  2. child ! "get"
  3. expectMsg(42)
  4.  
  5. child ! new ArithmeticException // зламати
  6. child ! "get"
  7. expectMsg(42)

Як ми бачимо, значення 42 витримує директиву обробки відмови. Тепер, якщо ми змінимо відмову до більш серйозної NullPointerException, це вже не буде таким:

  1. child ! new NullPointerException // зламаємо краще
  2. child ! "get"
  3. expectMsg(0)

Та нарешті, в випадку фатального IllegalArgumentException дитя буде завершене супервізором:

  1. watch(child) // нехай testActor наглядає за “child”
  2. child ! new IllegalArgumentException // ламаємо
  3. expectMsgPF() { case Terminated(`child`) => () }

До сих пір супервізор був повністю нечутливий до відмов дітей, оскільки набір директив обробляв їх. в випадку Exception, це вже не так, та супервізор ескалує відмову.

  1. supervisor ! Props[Child] // створити нове дитя
  2. val child2 = expectMsgType[ActorRef]
  3. watch(child2)
  4. child2 ! "get" // перевірити, чи воно живе
  5. expectMsg(0)
  6.  
  7. child2 ! new Exception("CRASH") // ескалувати відмову
  8. expectMsgPF() {
  9. case t @ Terminated(`child2`) if t.existenceConfirmed => ()
  10. }

Сам супервізор наглядається високорівневим актором, що провадиться від ActorSystem, що має політику по замовчанню рестартувати, в випадку всіх Exception (з поважним виключенням ActorInitializationException таActorKilledException). Оскільки директива по замовчанню в випадку рестарта вбивати дитей, ми очікуємо, що наші бідні діти не виживуть після цієї відмови.

У випадку, коли це не бажано (що залежить від випадку використання), нам треба використати інший супервізор, що перевизначає цю поведінку.

  1. class Supervisor2 extends Actor {
  2. import akka.actor.OneForOneStrategy
  3. import akka.actor.SupervisorStrategy._
  4. import scala.concurrent.duration._
  5.  
  6. override val supervisorStrategy =
  7. OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) {
  8. case _: ArithmeticException => Resume
  9. case _: NullPointerException => Restart
  10. case _: IllegalArgumentException => Stop
  11. case _: Exception => Escalate
  12. }
  13.  
  14. def receive = {
  15. case p: Props => sender() ! context.actorOf(p)
  16. }
  17. // перевизначити замовчання вбивати дітей під час рестарту
  18. override def preRestart(cause: Throwable, msg: Option[Any]) {}
  19. }

З таким батьком дитя переживатиме ескальований рестарт, як демонструє цей тест:

  1. val supervisor2 = system.actorOf(Props[Supervisor2], "supervisor2")
  2.  
  3. supervisor2 ! Props[Child]
  4. val child3 = expectMsgType[ActorRef]
  5.  
  6. child3 ! 23
  7. child3 ! "get"
  8. expectMsg(23)
  9.  
  10. child3 ! new Exception("CRASH")
  11. child3 ! "get"
  12. expectMsg(0)

Диспечери

Akka MessageDispatcher - це те, що робить акторів Akka Actors "живими", це двигун машини, так що поговоримо про них. Всі реалізаціїMessageDispatcher є також ExecutionContext, що означає, що можуть бути використані для виконання довільного кода, наприклад, Future.

Диспечер по замовчанню

Кожна ActorSystem буде мати диспечер по замовчанню, що буде використаний, коли нічого іншого не сконфігуровано для Actor. Диспечер по замовчанню може бути сконфігурований, та по замовчанню Dispatcher з вказаним default-executor. Якщо ActorSystem створена з переданим ExecutionContext, цей ExecutionContext буде використаний як екзекутор по замовчанню для всіх диспечерів в цій ActorSystem. Якщо ExecutionContext не надано, він відкотиться назад до екзекутора, вказаного в akka.actor.default-dispatcher.default-executor.fallback. По замовчанню це "fork-join-executor", що дає чудову продуктивність в більшості випадків.

Пошук диспечера

Диспечери реалізують інтерфейс ExecutionContext, та можуть, таким чином, бути використані для  викликів Future  etc.

  1. // for use with Futures, Scheduler, etc.
  2. implicit val executionContext = system.dispatchers.lookup("my-dispatcher")

Встановлення диспечера для актора

В випадку, коли ви бажаєте надати вашому актору іншого диспечера, ніж по замовчанню, нам потрібно зробити дві речі, з яких перша є конфігурація диспечера:

  1. my-dispatcher {
  2. # Dispatcher є ім'ям базованого на подіях диспечера
  3. type = Dispatcher
  4. # Який тип ExecutionService використовувати
  5. executor = "fork-join-executor"
  6. # Конфіфгупація для пула fork-join
  7. fork-join-executor {
  8. # Мінімальне число потоків, щоб покрити число паралелізма на основі множника
  9. parallelism-min = 2
  10. # Паралелізм (потоки) ... ceil(available processors * factor)
  11. parallelism-factor = 2.0
  12. # Максимальне число потоків, щоб покрити число паралелізма на основі множника
  13. parallelism-max = 10
  14. }
  15. # Пропускна спроможність максимальне число повідомлень, що будуть оброблятись
  16. # на кожного актора, перед тим, як потік перестрибне на нового актора.
  17. # Встановіть 1 для максимальнї чесності.
  18. throughput = 100
  19. }

Зауваження

Зауважте, що parallelism-max не встановлює верхню межу не загальне число потоків, розміщених ForkJoinPool. Це налаштування особливо каже щодо числа гарячих потоків, що пул виконує, щоб зменшити затримку обробки нового надходящого завдання. Ви можете прочитати більше щодо паралелізма в документації JDK ForkJoinPool.

Та ось інший приклад, що використовує "thread-pool-executor":

  1. my-thread-pool-dispatcher {
  2. # Dispatcher є ім'ям базованого на подіях диспечера
  3. type = Dispatcher
  4. # Який тип ExecutionService використовувати
  5. executor = "thread-pool-executor"
  6. # Конфігурація для пула потоків
  7. thread-pool-executor {
  8. # Мінімальне число потоків, щоб покрити число паралелізма на основі множника
  9. core-pool-size-min = 2
  10. # Число потоків ядра ... ceil(available processors * factor)
  11. core-pool-size-factor = 2.0
  12. # Мінімальне число потоків, щоб покрити число паралелізма на основі множника
  13. core-pool-size-max = 10
  14. }
  15. # Пропускна спроможність максимальне число повідомлень, що будуть оброблятись
  16. # на кожного актора, перед тим, як потік перестрибне на нового актора.
  17. # Встановіть 1 для максимальнї чесності.
  18. throughput = 100
  19. }

Щодо інших опцій дивіться розділ default-dispatcher в Конфігурації

Потім ви створюєте актора як звичайно, та визначаєте диспечера в конфігурації розгортання.

  1. import akka.actor.Props
  2. val myActor = context.actorOf(Props[MyActor], "myactor")
  1. akka.actor.deployment {
  2. /myactor {
  3. dispatcher = my-dispatcher
  4. }
  5. }

Альтернативою до конфігурації розгортання є визначення диспечера до кода. Якщо ви визначаєте  dispatcher в конфігурації розгортання, тоді це значення буде використане, замість параметра, встановленого програмно.

  1. import akka.actor.Props
  2. val myActor =
  3. context.actorOf(Props[MyActor].withDispatcher("my-dispatcher"), "myactor1")

Зауваження

Диспечер, що ви задаєте в withDispatcher та властивість dispatcher в конфігурації розгортання, фактично є шляхом в вашій конфігурації . Так що в цьому прикладі це розділ вищого рівня, але ви можете, наприклад, покласти його як суб-секцію, де ви використовуєте крапки для позначення суб-секцій, ось так: "foo.bar.my-dispatcher"

Типи диспечерів

Є три різні типи диспечерів повідомлень:

Більше прикладів конфігурації диспечера

Конфігурація диспечера з фіксованим розміром пула потоків, тобто, для акторів, що виконують блокуючий IO:

  1. blocking-io-dispatcher {
  2. type = Dispatcher
  3. executor = "thread-pool-executor"
  4. thread-pool-executor {
  5. fixed-pool-size = 32
  6. }
  7. throughput = 1
  8. }

Та потім використовуємо його:

  1. val myActor =
  2. context.actorOf(Props[MyActor].withDispatcher("blocking-io-dispatcher"), "myactor2")

Конфігурація PinnedDispatcher:

  1. my-pinned-dispatcher {
  2. executor = "thread-pool-executor"
  3. type = PinnedDispatcher
  4. }

Та потім використовуємо її:

  1. val myActor =
  2. context.actorOf(Props[MyActor].withDispatcher("my-pinned-dispatcher"), "myactor3")

Зауважте, що ця конфігурація thread-pool-executor для приклада вище my-thread-pool-dispatcher НЕ прийнятна. Це тому, що кожний актор матиме свій власний пул потоків при використанні PinnedDispatcher, та цей пул буде мати тікльки один потік.

Зауважте, що не гарантовано, що той же потік буде використовуватись з часом, оскільки використовується таймаут ядра пула для PinnedDispatcher, щоб утримувати використання ресурсів низьким в випадку простоя акторів. Щоб використовувати той же потік весь час, вам треба додати  thread-pool-executor.allow-core-timeout=off до конфігурації PinnedDispatcher.

Поштові скриньки

В Akka Mailbox містить повідомлення, що призначені до Actor. Зазвичай, кожний Actor має свою власну скриньку, але, наприклад, в BalancingPool всі маршрути будуть поділяти один примірник поштової скриньки.

Вибір поштової скриньки

Запит типу черги повідомлень для актора

Можливо зробитит запити певний тип черги повідомлень для певного типа акторів, якщо цей актор розширятиме параметризований трейт RequiresMessageQueue. Ось приклад:

  1. import akka.dispatch.RequiresMessageQueue
  2. import akka.dispatch.BoundedMessageQueueSemantics
  3.  
  4. class MyBoundedActor extends MyActor
  5. with RequiresMessageQueue[BoundedMessageQueueSemantics]

Параметр типа до трейта RequiresMessageQueue потребує бути відображеним на поштову скриньку в конфігурації, наступним чином:

  1. bounded-mailbox {
  2. mailbox-type = "akka.dispatch.BoundedMailbox"
  3. mailbox-capacity = 1000
  4. mailbox-push-timeout-time = 10s
  5. }
  6.  
  7. akka.actor.mailbox.requirements {
  8. "akka.dispatch.BoundedMessageQueueSemantics" = bounded-mailbox
  9. }

Тепер кожного разу, коли ви створюєте актора з типом MyBoundedActor, він буде намагатись отримати обмежену поштову скриньку. Якщо актор має іншу скриньку, сконфігуровану для розгортання,  або напряму, або через диспечера з вказаним типом скриньки, тоді це перевизначить це відображення.

Зауваження

Тип черги в поштовій скриньці, створеній для актора, буде перевірятись з запитаним типом в трейті, та якщо черга не реалізує потрібний тип, тоді створення актора зазнає невдачі.

Запит на тип черги повідомлень для диспечера

Диспечер також може мати запит на тип поштової скриньки для акторів, що роблять на ньому. Прикладом є BalancingDispatcher, що потребує чергу повідомлень, що потік-безпечні для декількох конкурентних споживачів. Така вимога формулюється в розділі конфігурації диспечера, таким чином:

  1. my-dispatcher {
  2. mailbox-requirement = org.example.MyInterface
  3. }

Отримана вимога називає клас або інтерфейс, що буде потім гарантованим супертипом реалізації черги повідомлень. Ви випадку конфлікта — тобто, якщо актор потребує  тип скриньки, що не задовільняє цій вимозі — тоді створення актора зазнає невдачі.

Як обирається тип поштової скриньки

При стоворенні актора ActorRefProvider спочатку визначає диспечера, що буде його виконувати. Потім поштова скринька визначається наступним чином:

  1. Якщо конфігурація розгортання актора містить ключ mailbox, тоді використовуються він вказує  розділ конфігурації, що описує тип поштової скриньки, яка буде використовуватись.
  2. Якщо Props актора містить вибір поштової скриньки — тобто, на ній був викликаний withMailbox — тоді він вказує на розділ конфігурації, що описує тип поштової скриньки, яка буде використовуватись.
  3. Якщо розділ конфігурації диспечера містить ключ mailbox-type , той же розділ буде використано для конфігурації поштової скриньки.
  4. Якщо актор потребує тип поштової скриньки, як описано вище, тоді для визначення потрібного типу поштової скриньки буде викостано відображення цієї вимоги; якщо це зазнає невдачі, тоді замість цього буде спробована вимога диспечера — якщо є.
  5. Якщо диспечер потребує тип поштової скриньки, як описано вище, тоді для визначення типу поштової скриньки буде використано відображення для цієї вимоги.
  6. Інакше буде використана скринька по замовчанню akka.actor.default-mailbox.

Поштова скринька по замовчанню

Коли поштова скринька не вказана, як описано вище, використовується поштова скринька по замовчанню. По замовчанню це необмежена поштова скринька, на основі java.util.concurrent.ConcurrentLinkedQueue.

SingleConsumerOnlyUnboundedMailbox є навіть більш ефективною скринькою, та вона може бути використовуватись як скринька по замовчанню, але вона не може бути використана з BalancingDispatcher.

Конфігуарція SingleConsumerOnlyUnboundedMailbox як скриньки по замовчанню:

  1. akka.actor.default-mailbox {
  2. mailbox-type = "akka.dispatch.SingleConsumerOnlyUnboundedMailbox"
  3. }

Яка конфігурація до типу поштової скриньки

Кожна поштова скринька реалізована класом, що розширює MailboxType , та приймає два аргументи конструктора: об'єкт ActorSystem.Settings та розділаConfig. Останній обчислюється через отримання поіменованого розділу конфігурації від конфігурації системи акторів, перевизначаючи його ключ id  на шлях конфігурацїі типу поштової скриньки, та додаючи відкат до розділу конфігурації поштової скриньки по замовчанню.

Вбудовані реалізації поштової скриньки

Akka іде з декількома реалізаціями поштової скриньки:

Інші реалізації обмежених поштових скриньок, що будуть блокувати надсилача, якщо досягнута ємність, та сконфігуровано з ненульовим mailbox-push-timeout-time.

Зауваження

Наступні поштові скриньки повинні використовуватись з нульовим mailbox-push-timeout-time.

Приклади конфігурації поштових скриньок

PriorityMailbox

Як створити PriorityMailbox:

  1. import akka.dispatch.PriorityGenerator
  2. import akka.dispatch.UnboundedStablePriorityMailbox
  3. import com.typesafe.config.Config
  4.  
  5. // В цьому випадку ми наслідуємо від UnboundedStablePriorityMailbox
  6. // та запускаємо його з генератором преоритетов
  7. class MyPrioMailbox(settings: ActorSystem.Settings, config: Config)
  8. extends UnboundedStablePriorityMailbox(
  9. // Створюємо новий PriorityGenerator, нижчий prio означає біль важливе
  10. PriorityGenerator {
  11. // Повідомлення 'highpriority повинно трактувати першим,якщо можливо
  12. case 'highpriority => 0
  13.  
  14. // Повідомлення 'lowpriority повинно трактувати першим,якщо можливо
  15. case 'lowpriority => 2
  16.  
  17. // PoisonPill, якщо немає інших
  18. case PoisonPill => 3
  19.  
  20. // Ми ставимо 1 по замовчанню, що між високим та низким
  21. case otherwise => 1
  22. })

Та потім додаємо до конфігурації:

  1. prio-dispatcher {
  2. mailbox-type = "docs.dispatcher.DispatcherDocSpec$MyPrioMailbox"
  3. // Інша конфігуарція диспечера знаходиться тут
  4. }

Та ось приклад, як ви будете використовувати це:

  1. // Створюємо нового актора, що тільки друкує те, що обробляє
  2. class Logger extends Actor {
  3. val log: LoggingAdapter = Logging(context.system, this)
  4.  
  5. self ! 'lowpriority
  6. self ! 'lowpriority
  7. self ! 'highpriority
  8. self ! 'pigdog
  9. self ! 'pigdog2
  10. self ! 'pigdog3
  11. self ! 'highpriority
  12. self ! PoisonPill
  13.  
  14. def receive = {
  15. case x => log.info(x.toString)
  16. }
  17. }
  18. val a = system.actorOf(Props(classOf[Logger], this).withDispatcher(
  19. "prio-dispatcher"))
  20.  
  21. /*
  22. * Logs:
  23. * 'highpriority
  24. * 'highpriority
  25. * 'pigdog
  26. * 'pigdog2
  27. * 'pigdog3
  28. * 'lowpriority
  29. * 'lowpriority
  30. */

Також можливо сконфігурувати тип поштової скриньки напряму, таким чином:

  1. prio-mailbox {
  2. mailbox-type = "docs.dispatcher.DispatcherDocSpec$MyPrioMailbox"
  3. // Інша конфігурація поштової скриньки знаходиться тут
  4. }
  5.  
  6. akka.actor.deployment {
  7. /priomailboxactor {
  8. mailbox = prio-mailbox
  9. }
  10. }

Та потім використовуємо її, або з розгортання, як тут:

  1. import akka.actor.Props
  2. val myActor = context.actorOf(Props[MyActor], "priomailboxactor")

або з кода:

  1. import akka.actor.Props
  2. val myActor = context.actorOf(Props[MyActor].withMailbox("prio-mailbox"))

ControlAwareMailbox

ControlAwareMailbox може бути дуже корисним, якщо актор потребує можливості отримувати контрольні повідомлення безпосередньо, не важливо, скільки інших повідомлень вже в поштовій скриньці.

Це може бути сконфігуровано таким чином:

  1. control-aware-dispatcher {
  2. mailbox-type = "akka.dispatch.UnboundedControlAwareMailbox"
  3. // Інша конфігурація диспечера знаходиться тут
  4. }

Контрольні повідомлення мають розширювати трейт ControlMessage:

  1. import akka.dispatch.ControlMessage
  2.  
  3. case object MyControlMessage extends ControlMessage

Та ось приклад як ви можете використовувати це:

  1. // Ми створюємо нового актора, що лише роздруковує те, що обробляє
  2. class Logger extends Actor {
  3. val log: LoggingAdapter = Logging(context.system, this)
  4.  
  5. self ! 'foo
  6. self ! 'bar
  7. self ! MyControlMessage
  8. self ! PoisonPill
  9.  
  10. def receive = {
  11. case x => log.info(x.toString)
  12. }
  13. }
  14. val a = system.actorOf(Props(classOf[Logger], this).withDispatcher(
  15. "control-aware-dispatcher"))
  16.  
  17. /*
  18. * Logs:
  19. * MyControlMessage
  20. * 'foo
  21. * 'bar
  22. */

Створення вашого власного типу поштової скриньки

Приклад вартий тисячі балачок:

  1. import akka.actor.ActorRef
  2. import akka.actor.ActorSystem
  3. import akka.dispatch.Envelope
  4. import akka.dispatch.MailboxType
  5. import akka.dispatch.MessageQueue
  6. import akka.dispatch.ProducesMessageQueue
  7. import com.typesafe.config.Config
  8. import java.util.concurrent.ConcurrentLinkedQueue
  9. import scala.Option
  10.  
  11. // Маркерний трейт, використаний для відображення вимог скриньки
  12. trait MyUnboundedMessageQueueSemantics
  13.  
  14. object MyUnboundedMailbox {
  15. // Реалізація MessageQueue
  16. class MyMessageQueue extends MessageQueue
  17. with MyUnboundedMessageQueueSemantics {
  18.  
  19. private final val queue = new ConcurrentLinkedQueue[Envelope]()
  20.  
  21. // це має бути реалізоване; черга використана як приклад
  22. def enqueue(receiver: ActorRef, handle: Envelope): Unit =
  23. queue.offer(handle)
  24. def dequeue(): Envelope = queue.poll()
  25. def numberOfMessages: Int = queue.size
  26. def hasMessages: Boolean = !queue.isEmpty
  27. def cleanUp(owner: ActorRef, deadLetters: MessageQueue) {
  28. while (hasMessages) {
  29. deadLetters.enqueue(owner, dequeue())
  30. }
  31. }
  32. }
  33. }
  34.  
  35. // Це реалізація Mailbox
  36. class MyUnboundedMailbox extends MailboxType
  37. with ProducesMessageQueue[MyUnboundedMailbox.MyMessageQueue] {
  38.  
  39. import MyUnboundedMailbox._
  40.  
  41. // Цей структура конструктора має існувати, вона викликається Akka
  42. def this(settings: ActorSystem.Settings, config: Config) = {
  43. // розташуйте тут ваш код ініціалізації
  44. this()
  45. }
  46.  
  47. // Метод create викликається для створення MessageQueue
  48. final override def create(
  49. owner: Option[ActorRef],
  50. system: Option[ActorSystem]): MessageQueue =
  51. new MyMessageQueue()
  52. }

Та потім ви просто вказуєте FQCN до вашого MailboxType, як значення "mailbox-type" в конфігурації диспечера, або в конфігурації поштової скриньки.

Зауваження

Переконайтесь, що включили конструктор, що приймає аргументи  akka.actor.ActorSystem.Settings та com.typesafe.config.Config, бо цей конструктор викликається рефлекційно для конструювання вашого типу поштової скриньки. Кофігурація, передана як другий аргумент, є тим розділом конфігурації, що описує налаштування диспечера або скриньки з використанням цього типу скриньки; тип скриньки буде утворений по разу для кожного диспечера або скриньки, що встановлюється з його використанням.

Ви також можете використовувати поштову скриньку як вимогу до диспечера, ось так:

  1. custom-dispatcher {
  2. mailbox-requirement =
  3. "docs.dispatcher.MyUnboundedJMessageQueueSemantics"
  4. }
  5.  
  6. akka.actor.mailbox.requirements {
  7. "docs.dispatcher.MyUnboundedJMessageQueueSemantics" =
  8. custom-dispatcher-mailbox
  9. }
  10.  
  11. custom-dispatcher-mailbox {
  12. mailbox-type = "docs.dispatcher.MyUnboundedJMailbox"
  13. }

Або визначаючи вимогу на вашому класі актора, ось так:

  1. class MySpecialActor extends Actor
  2. with RequiresMessageQueue[MyUnboundedMessageQueueSemantics] {
  3. // ...
  4. }

Спеціальна семантика system.actorOf

Щоб зробити system.actorOf одночасно синхронним та неблокуючим, та при тому підтримувати тип результата ActorRef (та семантику, коли повернутий ref повністю функціональний), для цього випадка має місце особлива обробка. Поза сценою створюється порожній тип посилання на актора, що надсилається системному актору-охоронцю, що насправді створює актора та його контекст, та покладає це в посилання. Докі це не відбудеться, повідомлення, надіслані на ActorRef, будуть  локально поставлені в чергу, та тільки після заміни реального заповнення вони будуть передані в реальну поштову скриньку. Таким чином,

  1. val props: Props = ...
  2. // цей актор використовує MyCustomMailbox, що, за очікуванням, є синглтоном
  3. system.actorOf(props.withDispatcher("myCustomMailbox")) ! "bang"
  4. assert(MyCustomMailbox.instance.getLastEnqueuedMessage == "bang")

це, напевне, схибить; вам буде потрібно надати деякий час, щоб передати та повторити перевірку, в дусі TestKit.awaitCond.

Маршрутизація

Повідомлення можуть бути надіслані через маршрутизатор, щоб ефективно передати їх до цільових акторів, відомих як його призначенняRouter може бути використаний в середині або ззовні актора, та ви можете керувати призначеннями власноруч, або використовувати самодостатній актор-маршрутизатор з можливостями конфігурації.

Можуть бути використані різні стратегії, відповідно до потреб вашого застосування. Akka іде з декількома корисними стратегіями маршрутизації прямо з коробки. Але, як ви побачите в цій главі, також можливо створити вашу власну.

Простий маршрутизатор

Наступний приклад ілюструє, як використовувати Router та керувати призначеннями з актора.

  1. import akka.routing.{ ActorRefRoutee, RoundRobinRoutingLogic, Router }
  2.  
  3. class Master extends Actor {
  4. var router = {
  5. val routees = Vector.fill(5) {
  6. val r = context.actorOf(Props[Worker])
  7. context watch r
  8. ActorRefRoutee(r)
  9. }
  10. Router(RoundRobinRoutingLogic(), routees)
  11. }
  12.  
  13. def receive = {
  14. case w: Work =>
  15. router.route(w, sender())
  16. case Terminated(a) =>
  17. router = router.removeRoutee(a)
  18. val r = context.actorOf(Props[Worker])
  19. context watch r
  20. router = router.addRoutee(r)
  21. }
  22. }

Ми створили Router та вказали, що він повинен використовувати RoundRobinRoutingLogic при маршрутизації до пунктів призначення.

Логіка маршрутизації, що надходить з Akka, така:

Ми сворили призначення як звичайні діти-актори, огорнуті в ActorRefRoutee. Ми наглядаємо за призаченнями, щоб бути в змозі замінити їх, якщо вони завершаться.

Надсилання повідомлень через маршрутизатор робиться через метод route, як це робиться для повідомлень Work в прикладі вище.

Router є незмінним та RoutingLogic стійка до потоків; це означає, що також можуть використовуватись за межами акторів.

Зауваження

Загалом кожне повідомлення, надіслане до маршрутизатора, буде надіслане до його призначень, але є одне виключення. Особливі Широкополосні повідомлення будуть адіслані до всіх пунктів призначення

Актор-маршрутизатор

Маршрутизатор також може бути створений як самодостатній актор, що керує призначеннями, на завантажує логіку маршрутизації та інші налаштування з конфігурації.

Цей тип актора-маршрутизатора іде в двох різних варіантах:

Налаштування для актора-маршрутизатора можуть бути визначені в конфігурації або програмно. Хоча актори-маршрутизатори можуть бути визначені в файлі конфігурації, вони все ще мають створюватись програмно, тобто, ви не можете зробити маршрутизатор тільки через зовнішній файл. Якщо ви визначите актора-маршрутизатор в файлі конфігурації, тоді ці налаштування будуть використані замість любих програмно наданих параметрів.

Ви надсилаєте повідомлення до призначень через актор-маршрутизатор в той же спосіб, як і для звичайних асторів, тобто через його ActorRef. Актори-маршрутизатори пересилають повідомлення до призначень без зміни первинного надсилача. Коли призначення відповідає на маршрутизоване повідомлення, відповідь буде доставлена первинному надсилачу, та не до актора-маршрутизатора.

Зауваження

Загалом, любе повідомлення, надіслане до маршрутизатора, буде надіслане до його призначень, але є декілька виключень. Вони задокументовані в розділі нижче,  Спеціальна обробка повідомлень.

Пул

Наступний код та клаптики конфігурації показують, як створити круговий (round-robin) маршрутизатор, що пересилає повідомлення до п'яти робочих призначень Worker. Призначення будуть створені як діти маршрутизатора.

  1. akka.actor.deployment {
  2. /parent/router1 {
  3. router = round-robin-pool
  4. nr-of-instances = 5
  5. }
  6. }
  1. val router1: ActorRef =
  2. context.actorOf(FromConfig.props(Props[Worker]), "router1")

А ось той же приклад, але з налаштуванням маршрутизатора, що надається програмно, замість походити з конфігурації.

  1. val router2: ActorRef =
  2. context.actorOf(RoundRobinPool(5).props(Props[Worker]), "router2")

Призначення з віддаленим розташуванням

На додаток до змоги створювати локальні актори в якості пунктів призначення, ви можете проінструктувати маршрутизатор розташувати створених їм дітей на наборі віддалених вузлів. Призначення будуть розгорнуті в стилі по-колу. Щоб розмістити призначення віддалено, огорніть конфігурацію маршрутизатора в RemoteRouterConfig, додаючи віддалені адреси вузлів до розташування. Віддалене розгортання потребує включення в classpath модуля akka-remote.

  1. import akka.actor.{ Address, AddressFromURIString }
  2. import akka.remote.routing.RemoteRouterConfig
  3. val addresses = Seq(
  4. Address("akka.tcp", "remotesys", "otherhost", 1234),
  5. AddressFromURIString("akka.tcp://othersys@anotherhost:1234"))
  6. val routerRemote = system.actorOf(
  7. RemoteRouterConfig(RoundRobinPool(5), addresses).props(Props[Echo]))

Надсилачі

По замовчанню, коли актор-призначення надсилає повідомлення, він буде неявно вказувати себе як надсилача.

  1. sender() ! x // відповіді будуть надходити до цього актора

Однак, також часто корисно для призначень вказувати маршрутизатор як надсилача. Наприклад, ви можете побажати встановити маршрутизатор як надсилача, якщо ви бажаєте приховати деталі призначень за маршрутизатором. Наступний код показує, як встановити батьківський маршрутизатор в якості надсилача.

  1. sender().tell("reply", context.parent) // відповіді надійдуть до батька
  2. sender().!("reply")(context.parent) // альтернативний синтаксис (зауважте дужки!)

Нагляд

Призначення, що створені маршрутизатором пула, будуть створені як діти маршрутизатора. Таким чином, маршрутизатор є супервізором дітей.

Стратегія супервізора актора-маршрутизатора може бути сконфігурована з допомогою властивості supervisorStrategy для Pool. Якщо конфігурація не запроваджена, маршрутизатор по замовчанню обирає стратегію “завжди ескалувати”. Це означає, що помилки передаються на обробку догори, до супервізора маршрутизатора. Супервізор маршрутизатора буде вирішувати, що робити з ціма помилками.

Зауважте, що супервізор маршрутизатора буде розглядати помилки як помилки самого маршрутизатора. Таким чином, вказівка зупинити або рестартувати спричинить зупинку або рестарт самого маршрутизатора. Маршрутизатор, зі свого боку, викличе зупинку або рестарт своїх дітей.

Треба зазначити, що поведінка рестарта маршрутизатора була перекрита, так що рестарт, хоча все ще пере-створює дітей, буде зберігати ту ж кількість акторів в пулі. 

Це означає, що якщо ви не вказали в supervisorStrategy маршрутизатора або його батька, збій в призначенні буде ескалувати до батька маршрутизатоа, що буде по замовчанню рестартувати маршрутизатор, що також рестартує всі призначення (використовується Escalate, та не зупиняє призначення під час рестарта). Причина зробити поведінку по замовчанню саме такою в тому, що додавання withRouter до визначення дитини не змінює стратегію нагляду, застосовану для дитини. Це може бути неефективністю, що ви можете уникнути, задавши стратегію при визначенні маршрутизатора.

Завдання стратегії робиться просто:

  1. val escalator = OneForOneStrategy() {
  2. case e testActor ! e; SupervisorStrategy.Escalate
  3. }
  4. val router = system.actorOf(RoundRobinPool(1, supervisorStrategy = escalator).props(
  5. routeeProps = Props[TestActor]))

Зауваження

Якщо дитина кругового маршрутизатора завершуєтсья, круговий маршрутизатор не буде відгалужувати нову дитину. В випадку, коли всі діти маршрутизатора пула завершаться, завершиться і самий маршрутизатор, якщо це тільки не динамічний маршрутизатор, що використовує ресайзер.

Група

Іноді, замість мати актора-маршрутизатор, що створює свої призначення, бажано створити призначення окремо, та запровадити їх до маршрутизатора для використання. Ви можете зробити це, передаючи шляхи призначень до конфігурації маршрутизатора. Повідомлення будуть надіслані за допомогою ActorSelection по цім шляхам.

Приклад нижче показує, як створити маршрутизатор, провадячи йому рядки шляхів трьох акторів-призначень.

  1. akka.actor.deployment {
  2. /parent/router3 {
  3. router = round-robin-group
  4. routees.paths = ["/user/workers/w1", "/user/workers/w2", "/user/workers/w3"]
  5. }
  6. }
  1. val router3: ActorRef =
  2. context.actorOf(FromConfig.props(), "router3")

Ось той же приклад, але конфігурація маршрутизатора запроваджена програмно, замість конфігурації.

  1. val router4: ActorRef =
  2. context.actorOf(RoundRobinGroup(paths).props(), "router4")

Актори-призначення створені ззовні відносно маршрутизатора:

  1. system.actorOf(Props[Workers], "workers")
  1. class Workers extends Actor {
  2. context.actorOf(Props[Worker], name = "w1")
  3. context.actorOf(Props[Worker], name = "w2")
  4. context.actorOf(Props[Worker], name = "w3")
  5. // ...

Шлях може містити інформацію про протокол та адресу акторів, що роблять на віддалених вузлах. Віддалені зв'язки потребують модуля akka-remote, включеного в classpath.

  1. akka.actor.deployment {
  2. /parent/remoteGroup {
  3. router = round-robin-group
  4. routees.paths = [
  5. "akka.tcp://app@10.0.0.1:2552/user/workers/w1",
  6. "akka.tcp://app@10.0.0.2:2552/user/workers/w1",
  7. "akka.tcp://app@10.0.0.3:2552/user/workers/w1"]
  8. }
  9. }

Використання маршрутизатора

В цьому розділі ми опишемо, як створити різні типи акторів-маршрутизаторів.

Актор-маршрутизатор в цьому розділі створений в акторі вищого рівня на ім'я parent. Зауважте, що шлях розгортання в конфігурації починається з /parent/, за яким слідує ім'я актора-маршрутизатора.

  1. system.actorOf(Props[Parent], "parent")

RoundRobinPool та RoundRobinGroup

Маршрутизує по колу до своїх призначень.

RoundRobinPool, визначений в конфігурації:

  1. akka.actor.deployment {
  2. /parent/router1 {
  3. router = round-robin-pool
  4. nr-of-instances = 5
  5. }
  6. }
  1. val router1: ActorRef =
  2. context.actorOf(FromConfig.props(Props[Worker]), "router1")

RoundRobinPool, визначений в коді:

  1. val router2: ActorRef =
  2. context.actorOf(RoundRobinPool(5).props(Props[Worker]), "router2")

RoundRobinGroup, визначений в конфігурації:

  1. akka.actor.deployment {
  2. /parent/router3 {
  3. router = round-robin-group
  4. routees.paths = ["/user/workers/w1", "/user/workers/w2", "/user/workers/w3"]
  5. }
  6. }
  1. val router3: ActorRef =
  2. context.actorOf(FromConfig.props(), "router3")

RoundRobinGroup, визначений в коді:

  1. val paths = List("/user/workers/w1", "/user/workers/w2", "/user/workers/w3")
  2. val router4: ActorRef =
  3. context.actorOf(RoundRobinGroup(paths).props(), "router4")

RandomPool та RandomGroup


Цей тип маршрутизатора обирає одного зі своїх призначень навмання для кожного повідомлення.

RandomPool, визначений в конфігурації:

  1. akka.actor.deployment {
  2. /parent/router5 {
  3. router = random-pool
  4. nr-of-instances = 5
  5. }
  6. }
  1. val router5: ActorRef =
  2. context.actorOf(FromConfig.props(Props[Worker]), "router5")

RandomPool, визначений в коді:

  1. val router6: ActorRef =
  2. context.actorOf(RandomPool(5).props(Props[Worker]), "router6")

RandomGroup, визначений в конфігурації:

  1. akka.actor.deployment {
  2. /parent/router7 {
  3. router = random-group
  4. routees.paths = ["/user/workers/w1", "/user/workers/w2", "/user/workers/w3"]
  5. }
  6. }
  1. val router7: ActorRef =
  2. context.actorOf(FromConfig.props(), "router7")

RandomGroup, визначений в коді:

  1. val paths = List("/user/workers/w1", "/user/workers/w2", "/user/workers/w3")
  2. val router8: ActorRef =
  3. context.actorOf(RandomGroup(paths).props(), "router8")

BalancingPool

Маршрутизатор, що буде намагатись перерозподілити роботу з навантажених на простоюючі призначення. Всі маршрути поділяють єдину поштову скриньку.

Зауваження

BalancingPool має таку властивість, що всі його призначення не мають справжньої різної ідентичності: вони мають різні назви, але розмова з ними не буде закінчуватись на вірному акторі в більшості випадків. Таким чином, ви не можете використовувати його для потоків виконання, що потребують зберігання стану на призначенні, ви маєте вкладати весь стан в повідомлення.

З допомогою SmallestMailboxPool ви можете мати вертикально маштабований сервіс, що може взаємодіяти в стилі стану з іншими сервісами бек-енду, перед тим, як відповісти оригінальному клієнту. Інша перевага в тому, що він не накладає обмеження на реалізацію черги повідомлень, як робить BalancingPool.

BalancingPool, визначений в конфігурації:

  1. akka.actor.deployment {
  2. /parent/router9 {
  3. router = balancing-pool
  4. nr-of-instances = 5
  5. }
  6. }
  1. val router9: ActorRef =
  2. context.actorOf(FromConfig.props(Props[Worker]), "router9")

BalancingPool, визначений в коді:

  1. val router10: ActorRef =
  2. context.actorOf(BalancingPool(5).props(Props[Worker]), "router10")

Додаткова конфігурація для балансуючого диспечера, що використовується пулом, може бути сконфігурована в розділі pool-dispatcher конфігурації розгортання маршрутизатора.

  1. akka.actor.deployment {
  2. /parent/router9b {
  3. router = balancing-pool
  4. nr-of-instances = 5
  5. pool-dispatcher {
  6. attempt-teamwork = off
  7. }
  8. }
  9. }

BalancingPool автоматично використовує спеціальний BalancingDispatcher для своїх призначень - незважаючи на жодний диспечер, що встановлений на об'єкті Props призначення. Це потрібно, щоб реалізувати семантику балансування через розділення однієї поштової скриньки для всіх призначень.

Хоча неможливо змінити диспечер, що використовуєть призначеннями, можливо гарно налаштувати використовуваний виконавець. по замовчанню використовується fork-join-dispatcher, та може бути сконфігурований, як пояснено в Диспечерах. В ситуаціях, коли призначення очікувано будуть виконувати блокуючі операції, може бути корисним замінити його на thread-pool-executor, явно підказуючи число розміщених потоків:

  1. akka.actor.deployment {
  2. /parent/router10b {
  3. router = balancing-pool
  4. nr-of-instances = 5
  5. pool-dispatcher {
  6. executor = "thread-pool-executor"
  7.  
  8. # розмістити рівно 5 потоків для цього пула
  9. thread-pool-executor {
  10. core-pool-size-min = 5
  11. core-pool-size-max = 5
  12. }
  13. }
  14. }
  15. }

Немає варіанту Group для BalancingPool.

SmallestMailboxPool

Маршрутизатор, що намагається надсилати до не-призупиненого дитячого призначення з найменшою кількістю повідомлень в скриньці. Вибір робиться в такому порядку:

  • береться любе простоююче призначення (що не обробляє повідомлення) з порожньою скринькою
  • береться любе пирзначення з порожньою скринькою
  • береться призначення з найменьшими підвислими повідомленнями в скриньці
  • береться любе віддалене призначення, віддалені актори мають найнижчий приоритет, оскільки розмір їх скриньки невідомий

SmallestMailboxPool, визначений в конфігурації:

  1. akka.actor.deployment {
  2. /parent/router11 {
  3. router = smallest-mailbox-pool
  4. nr-of-instances = 5
  5. }
  6. }
  1. val router11: ActorRef =
  2. context.actorOf(FromConfig.props(Props[Worker]), "router11")

SmallestMailboxPool, визначений в коді:

  1. val router12: ActorRef =
  2. context.actorOf(SmallestMailboxPool(5).props(Props[Worker]), "router12")

Немає варіанту Group для SmallestMailboxPool, оскільки розмір скриньки та внутрішній стан диспечеризації актора на практиці недоступний для шляхів призначень.

BroadcastPool та BroadcastGroup

Широкополосний маршрутизатор пересилає повідомлення, що отримує, до всіх призначень.

BroadcastPool, визначений в конфігурації:

  1. akka.actor.deployment {
  2. /parent/router13 {
  3. router = broadcast-pool
  4. nr-of-instances = 5
  5. }
  6. }
  1. val router13: ActorRef =
  2. context.actorOf(FromConfig.props(Props[Worker]), "router13")

BroadcastPool, визначений в коді:

  1. val router14: ActorRef =
  2. context.actorOf(BroadcastPool(5).props(Props[Worker]), "router14")

BroadcastGroup, визначений в конфігурації:

  1. akka.actor.deployment {
  2. /parent/router15 {
  3. router = broadcast-group
  4. routees.paths = ["/user/workers/w1", "/user/workers/w2", "/user/workers/w3"]
  5. }
  6. }
  1. val router15: ActorRef =
  2. context.actorOf(FromConfig.props(), "router15")

BroadcastGroup, визначений в коді:

  1. val paths = List("/user/workers/w1", "/user/workers/w2", "/user/workers/w3")
  2. val router16: ActorRef =
  3. context.actorOf(BroadcastGroup(paths).props(), "router16")

Зауваження

Широкополосні маршрутизатори завжди розповсюджують кожне повідомлення до своїх призначень. Якщо ви не бажаєте пересилати любе повідомлення, тоді ви можете використовувати не-широкополосний маршрутизатор, та використовувати Broadcast Messages за необхідністю.

ScatterGatherFirstCompletedPool та ScatterGatherFirstCompletedGroup

ScatterGatherFirstCompletedRouter буде надсилати повідомлення до всіх своїх пунктів призначень. Потім він очікує першу зворотню відповідь. Цей результат буде надісланий назад до оригінального надсилача. Інші відповіді будуть відкинуті.

Очікується щонайменьше одна відповідь в сконфігурований проміжок часу, інакше буде відповідь akka.pattern.AskTimeoutException в akka.actor.Status.Failure.

ScatterGatherFirstCompletedPool, визначений в конфігурації:

  1. akka.actor.deployment {
  2. /parent/router17 {
  3. router = scatter-gather-pool
  4. nr-of-instances = 5
  5. within = 10 seconds
  6. }
  7. }
  1. val router17: ActorRef =
  2. context.actorOf(FromConfig.props(Props[Worker]), "router17")

ScatterGatherFirstCompletedPool, визначений в коді:

  1. val router18: ActorRef =
  2. context.actorOf(ScatterGatherFirstCompletedPool(5, within = 10.seconds).
  3. props(Props[Worker]), "router18")

ScatterGatherFirstCompletedGroup, визначений в конфігурації:

  1. akka.actor.deployment {
  2. /parent/router19 {
  3. router = scatter-gather-group
  4. routees.paths = ["/user/workers/w1", "/user/workers/w2", "/user/workers/w3"]
  5. within = 10 seconds
  6. }
  7. }
  1. val router19: ActorRef =
  2. context.actorOf(FromConfig.props(), "router19")

ScatterGatherFirstCompletedGroup, визначений в коді:

  1. val paths = List("/user/workers/w1", "/user/workers/w2", "/user/workers/w3")
  2. val router20: ActorRef =
  3. context.actorOf(ScatterGatherFirstCompletedGroup(
  4. paths,
  5. within = 10.seconds).props(), "router20")

TailChoppingPool та TailChoppingGroup

TailChoppingRouter буде з початку надсилати повідомлення до одного, випадково обраного, призначення, та потім, через невелику затримку, до другого призначення (випадково обраного серед залишку), і так далі. Він очікує першу зворотню відповідь, та пересилає її назад до оригінального надсилача. Інші відповіді будуть відкинуті.

Ціллю цього маршрутизатора є зменшити затримку через виконання надмірних запитів до багатьох призначень, вважаючи, що один з інших акторів може бути швидшим щодо відповіді, ніж перший.

Ця оптимізація була гарно описана в блог пості Пітером Байлі: Виконання надмірної роботи для прискорення розподілених запитів.

TailChoppingPool, визначений в конфігурації:

  1. akka.actor.deployment {
  2. /parent/router21 {
  3. router = tail-chopping-pool
  4. nr-of-instances = 5
  5. within = 10 seconds
  6. tail-chopping-router.interval = 20 milliseconds
  7. }
  8. }
  1. val router21: ActorRef =
  2. context.actorOf(FromConfig.props(Props[Worker]), "router21")

TailChoppingPool, визначений в коді:

  1. val router22: ActorRef =
  2. context.actorOf(TailChoppingPool(5, within = 10.seconds, interval = 20.millis).
  3. props(Props[Worker]), "router22")

TailChoppingGroup, визначений в конфігурації:

  1. akka.actor.deployment {
  2. /parent/router23 {
  3. router = tail-chopping-group
  4. routees.paths = ["/user/workers/w1", "/user/workers/w2", "/user/workers/w3"]
  5. within = 10 seconds
  6. tail-chopping-router.interval = 20 milliseconds
  7. }
  8. }
  1. val router23: ActorRef =
  2. context.actorOf(FromConfig.props(), "router23")

TailChoppingGroup, визначений в коді:

  1. val paths = List("/user/workers/w1", "/user/workers/w2", "/user/workers/w3")
  2. val router24: ActorRef =
  3. context.actorOf(TailChoppingGroup(
  4. paths,
  5. within = 10.seconds, interval = 20.millis).props(), "router24")

ConsistentHashingPool та ConsistentHashingGroup

The ConsistentHashingPool використовує узгоджене хешування для обрання призначення на основі надісланого повідомлення. Ця глава дає гарний погляд на те, як реалізоване узгоджене хешування.

Є три шляхи для визначення того, які дані використовувати для узгодженого хеша.

Ці шляхи визначення ключа узгодженого хеша можуть використовуватись разом та одночасно для одного і того ж маршрутизатора. Першим перевіряється hashMapping.

Приклад кода:

  1. import akka.actor.Actor
  2. import akka.routing.ConsistentHashingRouter.ConsistentHashable
  3.  
  4. class Cache extends Actor {
  5. var cache = Map.empty[String, String]
  6.  
  7. def receive = {
  8. case Entry(key, value) => cache += (key -> value)
  9. case Get(key) => sender() ! cache.get(key)
  10. case Evict(key) => cache -= key
  11. }
  12. }
  13.  
  14. final case class Evict(key: String)
  15.  
  16. final case class Get(key: String) extends ConsistentHashable {
  17. override def consistentHashKey: Any = key
  18. }
  19.  
  20. final case class Entry(key: String, value: String)
  1. import akka.actor.Props
  2. import akka.routing.ConsistentHashingPool
  3. import akka.routing.ConsistentHashingRouter.ConsistentHashMapping
  4. import akka.routing.ConsistentHashingRouter.ConsistentHashableEnvelope
  5.  
  6. def hashMapping: ConsistentHashMapping = {
  7. case Evict(key) => key
  8. }
  9.  
  10. val cache: ActorRef =
  11. context.actorOf(ConsistentHashingPool(10, hashMapping = hashMapping).
  12. props(Props[Cache]), name = "cache")
  13.  
  14. cache ! ConsistentHashableEnvelope(
  15. message = Entry("hello", "HELLO"), hashKey = "hello")
  16. cache ! ConsistentHashableEnvelope(
  17. message = Entry("hi", "HI"), hashKey = "hi")
  18.  
  19. cache ! Get("hello")
  20. expectMsg(Some("HELLO"))
  21.  
  22. cache ! Get("hi")
  23. expectMsg(Some("HI"))
  24.  
  25. cache ! Evict("hi")
  26. cache ! Get("hi")
  27. expectMsg(None)

В прикладі вище ви бачите, що повідомлення Get реалізує саме ConsistentHashable, докі повідомлення Entry огорнуте в ConsistentHashableEnvelope. Повідомлення Evict ообробляється через часткову функцію hashMapping.

ConsistentHashingPool, визначений в конфігурації:

  1. akka.actor.deployment {
  2. /parent/router25 {
  3. router = consistent-hashing-pool
  4. nr-of-instances = 5
  5. virtual-nodes-factor = 10
  6. }
  7. }
  1. val router25: ActorRef =
  2. context.actorOf(FromConfig.props(Props[Worker]), "router25")

ConsistentHashingPool, визначений в коді:

  1. val router26: ActorRef =
  2. context.actorOf(
  3. ConsistentHashingPool(5).props(Props[Worker]),
  4. "router26")

ConsistentHashingGroup, визначений в конфігурації:

  1. akka.actor.deployment {
  2. /parent/router27 {
  3. router = consistent-hashing-group
  4. routees.paths = ["/user/workers/w1", "/user/workers/w2", "/user/workers/w3"]
  5. virtual-nodes-factor = 10
  6. }
  7. }
  1. val router27: ActorRef =
  2. context.actorOf(FromConfig.props(), "router27")

ConsistentHashingGroup, визначений в коді:

  1. val paths = List("/user/workers/w1", "/user/workers/w2", "/user/workers/w3")
  2. val router28: ActorRef =
  3. context.actorOf(ConsistentHashingGroup(paths).props(), "router28")

virtual-nodes-factor є числом віртуальних вузлів на одне призначення, що використовується в колі вузлів узгодженого хеша для підтримки більш рівномірного розподілу.

Повідомлення з особливою обробкою

Більшість повідомлень, що актори надсилають до маршрутизатора, будуть переслані згідно логіки маршрутизації. Але є декілька типів повідомлень, що мають особливу поведінку. 

Зауважте, що це особливі повідомлення, за винятком повідомлення Broadcast, обробляються тільки актором, що є окремим маршрутизатором, але не компонентом akka.routing.Router, описаним в Простий маршрутизатор.

Широкополосні повідомлення

Повідомлення Broadcast може використовуватись для надсилання повідомлення всім призначенням маршрутизатора. Коли маршрутизатор отримує повідомлення Broadcast, він розішле закладку цього повідомлення до всіх призначень, не важливо, як цей маршрутизатор буде звичайно пересилати свої повідомлення.

Приклад нижче показує, як ви можете використовувати повідомлення Broadcast для надсилання дуже важливого  повідомлення кожному пункту призначення маршрутизації.

  1. import akka.routing.Broadcast
  2. router ! Broadcast("Watch out for Davy Jones' locker")

В цьому прикладі маршрутизатор надсилає повідомлення Broadcast, виділяє його закладку ("Watch out for Davy Jones' locker"), та потім надсилає її до всіх призначень маршрутизатора. Це справа кожного актора обробити отриману закладку повідомлення.

Повідомлення PoisonPill

Повідомлення PoisonPill має спеціальну обробку для всіх акторів, включаючи маршрутизатори. Коли любий актор отримує повідомлення PoisonPill, цей актор буде зупинений. Дивіться документацію щодо PoisonPill для подробиць.

  1. import akka.actor.PoisonPill
  2. router ! PoisonPill

Для маршрутизатора, що звичайно передає повідомлення до призначень, важливо уявляти, що повідомлення  PoisonPill обробляються тільки маршрутизатором. Повідомлення PoisonPill, надіслані до маршрутизатора, не будуть переслані до призначень.

Однак повідомлення PoisonPill, надіслане маршрутизатору, все ще може впливати на призначення, оскільки воно буде зупиняти маршрутизатор, та при зупинці маршрутизатора він також зупинить своїх дітей. Зупинка дітей є звичайною поведінкою актора. Маршрутизатор буде зупиняти призначення, що він сворив як своїх дітей. Кожне дитя буде обробляти свої поточні повідомлення, та потім зупинені. Це може призвести до того, що деякі повідомлення не будуть оброблені. Дивіться документацію щодо Зупинки акторів для додаткової інформації.

Якщо ви бажаєте зупинити маршрутизатор та його призначення, але ви бужаєте спершу обробити всі повідомлення, що наразі в скриньках призначень, тоді ви не повинні надсилати повідомлення PoisonPill маршрутизатору. Замість ви повинні огорнути повідомлення PoisonPill всередині повідомлення Broadcast, так що кожне призначення буде отримувати повідомлення PoisonPill. Зауважте, що це зупинить всі призначення, навість якщо призначення не є дітьми маршрутизатора, тобто, коли призначення програмно надані маршрутизатору. 

  1. import akka.actor.PoisonPill
  2. import akka.routing.Broadcast
  3. router ! Broadcast(PoisonPill)

З кодом, показаним вище, кожне призначення буде отримувати повідомлення PoisonPill. Кожне призначення буде продовжувати обробляти свої повідомлення, як звичано, з часом оброблячи PoisonPill. Це призведе до зупинки призначення. Після того, як всі призначення будуть зупинені, маршрутизатор сам по собі буде зупинений автоматично, тільки якщо це не динамічний маршрутизатор, тобто з використанням ресайзера. 

Зауваження

Чудовий блог пост Брендона МакАдамса Розподілення навантаження в Akka - та завершення після цього дискутує більш детально, як повідомлення PoisonPill можуть використовуватись для зупинки маршрутизаторів та призначень.

Повідомлення Kill

Kill повідомлення є іншим типом повідомлень, що мають особливу обробку. Дивіться Вбивство актора щодо загальної інформації, як актори обробляють повідомлення Kill.

Коли повідомлення Kill надсилається до маршрутизатора, той обробляє повідомлення внутрішньо, та не посилає його своїм призначенням. Маршрутизатор буде викликати ActorKilledException та давати збій. Після цього він буде або відновлений, рестартований, або завершений, в залежності від дого, як налаштований його супервізор.

Призначення, що є дітями такого маршрутизатора, також будуть призупинені, та на них буде впливати директива супервізора, що стосується маршрутизатора. Призначення, що не є дитьми маршрутизатора, тобто ті, що були створені ззовні маршрутизатора, не зазнають вплива.

  1. import akka.actor.Kill
  2. router ! Kill

Як і з повідомленням PoisonPill, є різниця між вбивством маршрутизатора, що непрямо вбиває його дітей (які також є його призначеннями), та прямим вбивством призначень (деякі з яких можуть і не бути його дітьми). Щоб вбити призначення напряму, маршрутизатор повинен надіслати повідомлення Kill, огорнуте в повідомлення Broadcast.

  1. import akka.actor.Kill
  2. import akka.routing.Broadcast
  3. router ! Broadcast(Kill)

Керування повідомленнями

Ці керівні повідомлення можуть бути оброблені після інших повідомлень, так що коли ви надіслали   AddRoutee , за яким безпосередньо слідує звичайне повідомлення, ви не гарантовані, що призначення були змінені при пересиланні звичайного повідомлення. Якщо вам треба знати, чи ваші зміни були застосовані, надішліть AddRoutee, за яким GetRoutees, то коли ви отримаєте відповідь Routees, ви знатимите, що обробка зміни була застосована. 

Динамічно зростаючий пул

Більшість пулів можуть бути використані з фіксованим числом призначень, або зі стратегією ресайзинга, для динамічного налаштування числа призначень.

Є два типи ресайзерів: Resizer по замовчанню, та OptimalSizeExploringResizer.

Resizer по замовчанню

Ресайзер по замовчанню змінює ємність пула догори та вниз на основі тиску, що обчислюється як відсоток зайнятих призначень пула. Він збільшує пул, якщо тиск більше, ніж певний поріг, та відкатується назад, якщо тиск нижче, ніж нижній поріг. Обоє пороги налаштовуються. 

Пул з ресайзером по замовчанню, визначений в конфігурації:

  1. akka.actor.deployment {
  2. /parent/router29 {
  3. router = round-robin-pool
  4. resizer {
  5. lower-bound = 2
  6. upper-bound = 15
  7. messages-per-resize = 100
  8. }
  9. }
  10. }
  1. val router29: ActorRef =
  2. context.actorOf(FromConfig.props(Props[Worker]), "router29")

Декілька більше опцій конфігурації are available and described in akka.actor.deployment.default.resizersection of the reference Configuration.

Пул з ресайзером, що визначений в коді:

  1. val resizer = DefaultResizer(lowerBound = 2, upperBound = 15)
  2. val router30: ActorRef =
  3. context.actorOf(
  4. RoundRobinPool(5, Some(resizer)).props(Props[Worker]),
  5. "router30")

Також слід зазначити, що якщо ми визначаємо ``router`` в файлі конфігурації, тоді це значення буде використане замість любих програмно встановлених параметрів.

Ресайзер з пошуком оптимального розміру

 OptimalSizeExploringResizer змінює розмір пула до оптимального, що провадить найбільшу пропускну здібність повідомлень. 

Цей ресайзер краще робить, коли ви очікуєте, що функція розміру файла до продуктивності буде опуклою. Наприклад, коли ви маєте завдання, що прив'язані до CPU, оптимальний розмір прив'язаний до числа ядер CPU. Коли ваше завдання прив'язане до IO, оптимальний розмір прив'язаний до оптимального числа одночасних з'єднань до цього пристрою IO - тобто, кластер з еластичним пошуком з 4 вузлів може обробляти 4-8 одночасних запитів з оптимальною швидкістю.

Це досягається відстеженням пропускної здібності повідомлень для кожного розміру пула, та периодичне виконання наступних трьох операцій зміни розміру (по одній за раз):

When the pool is fully-utilized (i.e. all routees are busy), it randomly choose between exploring and optimizing. When the pool has not been fully-utilized for a period of time, it will downsize the pool to the last seen max utilization multiplied by a configurable ratio.

By constantly exploring and optimizing, the resizer will eventually walk to the optimal size and remain nearby. When the optimal size changes it will start walking towards the new one.

It keeps a performance log so it's stateful as well as having a larger memory footprint than the default Resizer. The memory usage is O(n) where n is the number of sizes you allow, i.e. upperBound - lowerBound.

Pool with OptimalSizeExploringResizer defined in configuration:

  1. akka.actor.deployment {
  2. /parent/router31 {
  3. router = round-robin-pool
  4. optimal-size-exploring-resizer {
  5. enabled = on
  6. action-interval = 5s
  7. downsize-after-underutilized-for = 72h
  8. }
  9. }
  10. }
  1. val router31: ActorRef =
  2. context.actorOf(FromConfig.props(Props[Worker]), "router31")

Several more configuration options are available and described in akka.actor.deployment.default.optimal-size-exploring-resizer section of the reference Configuration.

Note

Resizing is triggered by sending messages to the actor pool, but it is not completed synchronously; instead a message is sent to the “head” RouterActor to perform the size change. Thus you cannot rely on resizing to instantaneously create new workers when all others are busy, because the message just sent will be queued to the mailbox of a busy actor. To remedy this, configure the pool to use a balancing dispatcher, see Configuring Dispatchers for more information.

How Routing is Designed within Akka

On the surface routers look like normal actors, but they are actually implemented differently. Routers are designed to be extremely efficient at receiving messages and passing them quickly on to routees.

A normal actor can be used for routing messages, but an actor's single-threaded processing can become a bottleneck. Routers can achieve much higher throughput with an optimization to the usual message-processing pipeline that allows concurrent routing. This is achieved by embedding routers' routing logic directly in their ActorRef rather than in the router actor. Messages sent to a router's ActorRef can be immediately routed to the routee, bypassing the single-threaded router actor entirely.

The cost to this is, of course, that the internals of routing code are more complicated than if routers were implemented with normal actors. Fortunately all of this complexity is invisible to consumers of the routing API. However, it is something to be aware of when implementing your own routers.

Custom Router

You can create your own router should you not find any of the ones provided by Akka sufficient for your needs. In order to roll your own router you have to fulfill certain criteria which are explained in this section.

Before creating your own router you should consider whether a normal actor with router-like behavior might do the job just as well as a full-blown router. As explained above, the primary benefit of routers over normal actors is their higher performance. But they are somewhat more complicated to write than normal actors. Therefore if lower maximum throughput is acceptable in your application you may wish to stick with traditional actors. This section, however, assumes that you wish to get maximum performance and so demonstrates how you can create your own router.

The router created in this example is replicating each message to a few destinations.

Start with the routing logic:

  1. import scala.collection.immutable
  2. import java.util.concurrent.ThreadLocalRandom
  3. import akka.routing.RoundRobinRoutingLogic
  4. import akka.routing.RoutingLogic
  5. import akka.routing.Routee
  6. import akka.routing.SeveralRoutees
  7.  
  8. class RedundancyRoutingLogic(nbrCopies: Int) extends RoutingLogic {
  9. val roundRobin = RoundRobinRoutingLogic()
  10. def select(message: Any, routees: immutable.IndexedSeq[Routee]): Routee = {
  11. val targets = (1 to nbrCopies).map(_ => roundRobin.select(message, routees))
  12. SeveralRoutees(targets)
  13. }
  14. }

select will be called for each message and in this example pick a few destinations by round-robin, by reusing the existing RoundRobinRoutingLogic and wrap the result in a SeveralRoutees instance. SeveralRoutees will send the message to all of the supplied routes.

The implementation of the routing logic must be thread safe, since it might be used outside of actors.

A unit test of the routing logic:

  1. final case class TestRoutee(n: Int) extends Routee {
  2. override def send(message: Any, sender: ActorRef): Unit = ()
  3. }
  4.  
  5. val logic = new RedundancyRoutingLogic(nbrCopies = 3)
  6.  
  7. val routees = for (n <- 1 to 7) yield TestRoutee(n)
  8.  
  9. val r1 = logic.select("msg", routees)
  10. r1.asInstanceOf[SeveralRoutees].routees should be(
  11. Vector(TestRoutee(1), TestRoutee(2), TestRoutee(3)))
  12.  
  13. val r2 = logic.select("msg", routees)
  14. r2.asInstanceOf[SeveralRoutees].routees should be(
  15. Vector(TestRoutee(4), TestRoutee(5), TestRoutee(6)))
  16.  
  17. val r3 = logic.select("msg", routees)
  18. r3.asInstanceOf[SeveralRoutees].routees should be(
  19. Vector(TestRoutee(7), TestRoutee(1), TestRoutee(2)))

You could stop here and use the RedundancyRoutingLogic with a akka.routing.Router as described in A Simple Router.

Let us continue and make this into a self contained, configurable, router actor.

Create a class that extends Pool, Group or CustomRouterConfig. That class is a factory for the routing logic and holds the configuration for the router. Here we make it a Group.

  1. import akka.dispatch.Dispatchers
  2. import akka.routing.Group
  3. import akka.routing.Router
  4. import akka.japi.Util.immutableSeq
  5. import com.typesafe.config.Config
  6.  
  7. final case class RedundancyGroup(routeePaths: immutable.Iterable[String], nbrCopies: Int) extends Group {
  8.  
  9. def this(config: Config) = this(
  10. routeePaths = immutableSeq(config.getStringList("routees.paths")),
  11. nbrCopies = config.getInt("nbr-copies"))
  12.  
  13. override def paths(system: ActorSystem): immutable.Iterable[String] = routeePaths
  14.  
  15. override def createRouter(system: ActorSystem): Router =
  16. new Router(new RedundancyRoutingLogic(nbrCopies))
  17.  
  18. override val routerDispatcher: String = Dispatchers.DefaultDispatcherId
  19. }

This can be used exactly as the router actors provided by Akka.

  1. for (n <- 1 to 10) system.actorOf(Props[Storage], "s" + n)
  2.  
  3. val paths = for (n <- 1 to 10) yield ("/user/s" + n)
  4. val redundancy1: ActorRef =
  5. system.actorOf(
  6. RedundancyGroup(paths, nbrCopies = 3).props(),
  7. name = "redundancy1")
  8. redundancy1 ! "important"

Note that we added a constructor in RedundancyGroup that takes a Config parameter. That makes it possible to define it in configuration.

  1. akka.actor.deployment {
  2. /redundancy2 {
  3. router = "docs.routing.RedundancyGroup"
  4. routees.paths = ["/user/s1", "/user/s2", "/user/s3"]
  5. nbr-copies = 5
  6. }
  7. }

Note the fully qualified class name in the router property. The router class must extendakka.routing.RouterConfig (Pool, Group or CustomRouterConfig) and have constructor with onecom.typesafe.config.Config parameter. The deployment section of the configuration is passed to the constructor.

  1. val redundancy2: ActorRef = system.actorOf(
  2. FromConfig.props(),
  3. name = "redundancy2")
  4. redundancy2 ! "very important"

Configuring Dispatchers

The dispatcher for created children of the pool will be taken from Props as described in Dispatchers.

To make it easy to define the dispatcher of the routees of the pool you can define the dispatcher inline in the deployment section of the config.

  1. akka.actor.deployment {
  2. /poolWithDispatcher {
  3. router = random-pool
  4. nr-of-instances = 5
  5. pool-dispatcher {
  6. fork-join-executor.parallelism-min = 5
  7. fork-join-executor.parallelism-max = 5
  8. }
  9. }
  10. }

That is the only thing you need to do enable a dedicated dispatcher for a pool.

Note

If you use a group of actors and route to their paths, then they will still use the same dispatcher that was configured for them in their Props, it is not possible to change an actors dispatcher after it has been created.

The “head” router cannot always run on the same dispatcher, because it does not process the same type of messages, hence this special actor does not use the dispatcher configured in Props, but takes the routerDispatcher from theRouterConfig instead, which defaults to the actor system’s default dispatcher. All standard routers allow setting this property in their constructor or factory method, custom routers have to implement the method in a suitable way.

  1. val router: ActorRef = system.actorOf(
  2. // “head” router actor will run on "router-dispatcher" dispatcher
  3. // Worker routees will run on "pool-dispatcher" dispatcher
  4. RandomPool(5, routerDispatcher = "router-dispatcher").props(Props[Worker]),
  5. name = "poolWithDispatcher")

Note

It is not allowed to configure the routerDispatcher to be aakka.dispatch.BalancingDispatcherConfigurator since the messages meant for the special router actor cannot be processed by any other actor.


Кінцеві автомати (FSM)

Огляд

FSM (Finite State Machine) доступний як мікс-ін для акторів Akka Actor, та найкраще описаний в принципах розробки Erlang

FSM може бути описаний як набір відношень в формі:

State(S) x Event(E) -> Actions (A), State(S')

Ці відношення інтерпретуються в наступному значенні:

Якщо ми є в стані S, та трапляється подія E, нам треба виконати дії A, та зробити перехід до стану S'.

Простий приклад

Щоб продемонструвати більшість з можливостей трейта FSM, розглянемо актора, який буде отримувати та ставити в чергу повідомлення, коли вони надходять в вибуховому режимі, та надсилає їх після спаду напливу, або коли надійде запит скинути повідомлення.

Перше, вважатимемо, що все нижче використовує такі твердження імпорта:

  1. import akka.actor.{ ActorRef, FSM }
  2. import scala.concurrent.duration._

Контракт нашого актора-накопичувача “Buncher” поялгає в тому, що він приймає або продукує наступні повідомлення:

  1. // отримані повідомлення
  2. final case class SetTarget(ref: ActorRef)
  3. final case class Queue(obj: Any)
  4. case object Flush
  5.  
  6. // надіслані повідомлення
  7. final case class Batch(obj: immutable.Seq[Any])

SetTarget потрібний для початку роботи, що вказує призначення, куди треба передавати Batches; Queue буде додавати до внутрішньої черги, тоді як Flush буде визначати завершення навали.

  1. // стани
  2. sealed trait State
  3. case object Idle extends State
  4. case object Active extends State
  5.  
  6. sealed trait Data
  7. case object Uninitialized extends Data
  8. final case class Todo(target: ActorRef, queue: immutable.Seq[Any]) extends Data

Актор може знаходитись в двох станах: повідомлень немає в черзі (тобто Idle), або деякі є в черзі (тобто Active). Він буде знаходитись в активному стані доти, докі повідомлення будуть продовжувати надходити, та не буде запита на скидання. Початковий стан даних актора складається з посилання на цільового актора, куди надсилати пакунки, та справжня черга повідомлень. 

Тепер давайте поглянемо на скелет для нашого актора FSM:

  1. class Buncher extends FSM[State, Data] {
  2.  
  3. startWith(Idle, Uninitialized)
  4.  
  5. when(Idle) {
  6. case Event(SetTarget(ref), Uninitialized) =>
  7. stay using Todo(ref, Vector.empty)
  8. }
  9.  
  10. // transition опущено ...
  11.  
  12. when(Active, stateTimeout = 1 second) {
  13. case Event(Flush | StateTimeout, t: Todo) =>
  14. goto(Idle) using t.copy(queue = Vector.empty)
  15. }
  16.  
  17. // unhandled опущено ...
  18.  
  19. initialize()
  20. }

Базова стратегія полягає в декларуванні актора, міксуванні трейтаFSM, та вказанні можливих станів та значень даних як параметрів типа. В тілі актора використовується DSL для декларування машини станів:

  • startWith визначає початковий стан та початкові дані
  • потім іде одна декларація when(<state>) { ... } на стан, що обробляється (може потенційно бути декількома, передані часткові функції PartialFunction будуть конкатеновані за допомогою  orElse)
  • нарешті, все запускається з використанням initialize, що виконує перехід до початкового стану та встановлює таймери (якщо потрібно).

В цьому випадку ми починаємо в стані Idle та Uninitialized, де обробляються тільки повідомлення SetTarget();stay готує завершення обробки цього повідомлення, не покидаючи поточного стану, тоді як подифікатор using змушує FSM змінити внутрішній стан (що є Uninitialized в цій точці) на новий об'єкт Todo(), що містить посилання на цільового актора. Стан Active має задекларований таймаут стану, що означає, що якщо протягом однієї секунди не надійде жодного повідомленя, буде згенероване повідомлення FSM.StateTimeout. В цьому випадку це має той же ефект, що отримання команди Flush, а саме до переходу назад до стану Idle, та скидання внутрішньої черги в пустий вектор. Але як повідомлення стають в чергу? Оскільки це має робити однаково в обох станах, ми використовуємо факт, що кожна подія, що не оброблена блоком when() буде передана до блока whenUnhandled():

  1. whenUnhandled {
  2. // загальний стан для обох станів
  3. case Event(Queue(obj), t @ Todo(_, v)) =>
  4. goto(Active) using t.copy(queue = v :+ obj)
  5.  
  6. case Event(e, s) =>
  7. log.warning("отримане необроблений запит {} в стані {}/{}", e, stateName, s)
  8. stay
  9. }

Перший оброблений тут випадок додає запити Queue() до внутрішньої черги, та переходить до стану Active (це також очевидно залишає в стані Active, якщо ви вже там), але тільки якщо дані FSM не Uninitialized, коли отримується подіяQueue(). Інакше — та в усіх інших необроблених випадках — жругий випадок тільки журналює попередження, та не змінює внутрішній стан.

Одна недостаюча тут річ, це де насправді Batches надсилається до цілі, для якої ми задіяли механізм onTransition: ви можете задекларувати декілька таких блоків, та всі вони будуть намагатись спробувати щодо співпадаючої поведінки в випадку зміни стану (тобто, коли стан насправді змінюється).

  1. onTransition {
  2. case Active -> Idle =>
  3. stateData match {
  4. case Todo(ref, queue) => ref ! Batch(queue)
  5. case _ => // нічого не робити
  6. }
  7. }

Зворотній виклик переходу є частковою функцією, що сприймає в якості входу пару станів — поточний та наступний стан. Трейт FSM включає зручний екстрактор для цього в формі оператора стрілки, що природно нагадує вам напрямок зміни стану, що порівнюється. Під час зміни стану старі дані стану доступні через stateData, як показано, та нові дані стану будуть доступні як nextStateData.

Зауваження

Перехід на той самий стан може бути реалізований (коли ви зараз в стані S) з використанням goto(S) або stay(). Різниця між ними полягає в тому, що goto(S) буде надсилати подію S->S, що буде оброблена onTransition, тоді як stay() не буде.

Щоб перевірити, що це дійсно робить, досить просто написати тест з використанням Тестування системи акторів, що запакована з трейтами ScalaTest в AkkaSpec:

  1. import akka.actor.Props
  2. import scala.collection.immutable
  3.  
  4. object FSMDocSpec {
  5. // повідомлення та типи даних
  6. }
  7.  
  8. class FSMDocSpec extends MyFavoriteTestFrameWorkPlusAkkaTestKit {
  9. import FSMDocSpec._
  10.  
  11. // fsm код пропущено ...
  12.  
  13. "simple finite state machine" must {
  14.  
  15. "demonstrate NullFunction" in {
  16. class A extends FSM[Int, Null] {
  17. val SomeState = 0
  18. when(SomeState)(FSM.NullFunction)
  19. }
  20. }
  21.  
  22. "batch correctly" in {
  23. val buncher = system.actorOf(Props(classOf[Buncher], this))
  24. buncher ! SetTarget(testActor)
  25. buncher ! Queue(42)
  26. buncher ! Queue(43)
  27. expectMsg(Batch(immutable.Seq(42, 43)))
  28. buncher ! Queue(44)
  29. buncher ! Flush
  30. buncher ! Queue(45)
  31. expectMsg(Batch(immutable.Seq(44)))
  32. expectMsg(Batch(immutable.Seq(45)))
  33. }
  34.  
  35. "not batch if uninitialized" in {
  36. val buncher = system.actorOf(Props(classOf[Buncher], this))
  37. buncher ! Queue(42)
  38. expectNoMsg
  39. }
  40. }
  41. }

Посилання

Трейт та об'єкт FSM

Трейт FSM прямо наслідує від Actor, коли ви розширяєте FSM, ви маєте знати, що насправді ви створюєте актора:

  1. class Buncher extends FSM[State, Data] {
  2.  
  3. // тіло fsm ...
  4.  
  5. initialize()
  6. }

Зауваження

Трейт FSM визначає метод receive, що обробляє внутрішні методи та передає все інше через логіку FSM (згідно поточного стану). Коли ви перевизначаєте метод receive, пам'ятайте, що обробка таймаутів стану залежить від справжньої передачі повідомлень через логіку FSM.

Трейт FSM приймає два параметри:

  1. супертип для всіх назв станів, зазвичай запечатаний трейт з кейс об'єктами, що розширюють його,
  2. тип даних стану, що відстежуються самим модулем FSM.

Зауваження

Дані стану разом зі іменем стану описують внутрішній стан кінечного автомата; якщо ви пристаєте до цієї схеми, та не додаєте змінних полів до класу FSM, ви маєте перевагу робити всі зміни внутрішнього стану явними, в небагатьох гарно відомих місцях.

Визначення станів

Стан визначається одним або більше викликами метода when(<name>[, stateTimeout = <timeout>])(stateFunction).

Надане ім'я має бути об'єктом, що за типом сумісний з перишм параметром типу, переданому в трейт FSM. Цей об'єкт використовується як хеш ключ, так що ви маєте переконатись, що він достойно реалізує equals та hashCode; зокрема він має бути незмінним. Простішим випадком для ціх вимог є кейс об'єкти. 

Якщо наданий параметр stateTimeout, тоді всі переходи в цей стан, включаючи залишення в ньому, отримають цей таймаут по замовчанню. Ініціація переходу з явно заданим таймаутом може перекрити це замовчання, дивіться Ініціація Переходів для додаткової інформацїі. Таймаут стану може бути змінений під час обробки за допомогою setStateTimeout(state, duration). Це дозволяє конфігурацію під час виконання, тобто через зовнішнє повідомленя.

Аргумент stateFunction є PartialFunction[Event, State], що зручно надається з використанням внутрішнього синтаксису часткової функції, як продемонстровано нижче:

  1. when(Idle) {
  2. case Event(SetTarget(ref), Uninitialized) =>
  3. stay using Todo(ref, Vector.empty)
  4. }
  5.  
  6. when(Active, stateTimeout = 1 second) {
  7. case Event(Flush | StateTimeout, t: Todo) =>
  8. goto(Idle) using t.copy(queue = Vector.empty)
  9. }

The Event(msg: Any, data: D) case class is parameterized with the data type held by the FSM for convenient pattern matching.

Warning

It is required that you define handlers for each of the possible FSM states, otherwise there will be failures when trying to switch to undeclared states.

It is recommended practice to declare the states as objects extending a sealed trait and then verify that there is a whenclause for each of the states. If you want to leave the handling of a state “unhandled” (more below), it still needs to be declared like this:

  1. when(SomeState)(FSM.NullFunction)

Defining the Initial State

Each FSM needs a starting point, which is declared using

startWith(state, data[, timeout])

The optionally given timeout argument overrides any specification given for the desired initial state. If you want to cancel a default timeout, use None.

Unhandled Events

If a state doesn't handle a received event a warning is logged. If you want to do something else in this case you can specify that with whenUnhandled(stateFunction):

  1. whenUnhandled {
  2. case Event(x: X, data) =>
  3. log.info("Received unhandled event: " + x)
  4. stay
  5. case Event(msg, _) =>
  6. log.warning("Received unknown event: " + msg)
  7. goto(Error)
  8. }

Within this handler the state of the FSM may be queried using the stateName method.

ВАЖЛИВО: Цей обробник не накладається, тобто кожний виклик whenUnhandled заміщує попередньо встановлений обробник.

Ініціація зміни стану

Результатом кожного stateFunction має бути визначення наступного стану, або зупинка FSM, що описане в Зупинка зсередини. Визначення стану може бути або поточний стан, що задається директивою stay, або інший стан, що дає goto(state). Отриманий об'єкт дозволяє подальшу кваліфікацію шляхом надання наступних модифікаторів:

Всі модифікатори можуть бути зціплені, щоб досягти гароного та стислого опису:

  1. when(SomeState) {
  2. case Event(msg, _) =>
  3. goto(Processing) using (newData) forMax (5 seconds) replying (WillDo)
  4. }

Дужки насправді не потрібні у всіх випадках, але вони візуально розділяють модифікатори та їх аргументи, і, таким чином, роблять код навіть більш приємним для читання сторонніми.

Зауваження

Будь ласка зауважте, що твердження return не може використовуватись в блоках when, або подібних; це обмеження Scala. Або перепишіть ваш код за допомогою if () ... else ..., або винесіть його в визначення метода.

Моніторинг зміни стану

Переходи відбуваються "між станами" концептуально, що означає після любих дій, що ви поклали в блок обробки події; це очевидне, оскільки інший стан визначений значенням, що повертається логікою обробки події. Вам не треба турбуватись щодо точного порядку, що встановлює змінну стану, тому що все в акторі FSM все одно виконується в одному потоці.

Внутрішній мониторинг

До цього місця FSM DSL був сконцентрований на станах та подіях. Подвійний погляд полягає в тому, щоб розглядати це як серію трансформацій. Це досягаєтсья методом

onTransition(handler)

що асоціює дії з переходами, замість станій та подій. Обробник є частковою функцією, що приймає пару станів на вході; результуючий стан не потрібен, бо неможливо трансформувати перехід в процессі.

  1. onTransition {
  2. case Idle -> Active => setTimer("timeout", Tick, 1 second, repeat = true)
  3. case Active -> _ => cancelTimer("timeout")
  4. case x -> Idle => log.info("entering Idle from " + x)
  5. }

Зручний екстрактор -> дозволяє докомпозицію пари станів з ясним візуальним нагадуванням напрямку переходу. Як звичайно в порівнянні шаблонів, може використовуватись підкреслення для нецікавих частей; альтернативно ви можете прікрипити необмежений стан до змінної, наприклад, для журналювання, як показане в останньому випадку.

Також можливо передати об'єкт-функцію, що приймає два стани, до onTransition, в випадку, коли ваша логіка переходу реалізована як метод:

  1. onTransition(handler _)
  2.  
  3. def handler(from: StateType, to: StateType) {
  4. // handle it here ...
  5. }

Зареєстровані таким чином обробники накладаються, так що ви можете перемежати блоки onTransition з блоками when, що відповідає вашому дизайну. Однак слід зауважити, що всі блоки будуть задіяні для кожного переходу, не тільки перший співпавший блок. Це зроблено навмисне, так що ви можете покласти всю обробку переходу для окремого аспекта в одне місце, при цьому не турбуючись, що ранні декларації затінять наступні; при цьому дії виконуються в порядку декларування.

Зауваження

Цей тип внутрішнього моніторингу може бути використаний для структурування вашого FSM відповідно до переходів, так що, наприклад, відміна таймера під час виходу з певного стану не може бути забута, коли додаються нові цільові стани. 

Зовнішній моніторинг

Зовнішні актори можуть бути зареєстровані для нотифікації про стан переходів, через надсилання повідомлення SubscribeTransitionCallBack(actorRef). Названому актору буде негайно надіслане повідомлення CurrentState(self, stateName), та він буде отримувати повідомлення  Transition(actorRef, oldState, newState) при перемиканні зміни стану.

Будь ласка зауважте, що зміна стану включає дію по виконанню goto(S), тоді, коли ви вже в станіS. В цьому випадку моніторячий актор буде повідомлений за допомогою повідомлення Transition(ref,S,S). Це може бути корисним, якщо ваш FSM повинен реагувати на всі переходи (навіть без зміни стану). В випадку, коли ви скоріше не бажаєте генерації подій для переходів в той же стан, використовуйте stay() замість goto(S).

Зовнішні монітори можуть бути відреєстровані через надсилання UnsubscribeTransitionCallBack(actorRef) до FSM актора.

Зупинка слухача без відреєстрації не видалить слухача зі списку підписки; використовуйте UnsubscribeTransitionCallback перед зупинкою слухача.

Стан переходу

Часткова функція, надана як аргумент до блоку  when() може бути перетворена з використанням повної підтримки функціональних програмних інструментів Scala. Існує допоміжня функція, що може бути використана в випадку деякої загальної логіки обробки, що має застосовуватись в різних випадках:

  1. when(SomeState)(transform {
  2. case Event(bytes: ByteString, read) => stay using (read + bytes.length)
  3. } using {
  4. case s @ FSM.State(state, read, timeout, stopReason, replies) if read > 1000 =>
  5. goto(Processing)
  6. })

Ми не кажемо про те, що аргументи до цього метода можуть також бути збережені, використані декілька раз, тобто коли один перехід застосовується до декількох блоків  when() :

  1. val processingTrigger: PartialFunction[State, State] = {
  2. case s @ FSM.State(state, read, timeout, stopReason, replies) if read > 1000 =>
  3. goto(Processing)
  4. }
  5.  
  6. when(SomeState)(transform {
  7. case Event(bytes: ByteString, read) => stay using (read + bytes.length)
  8. } using processingTrigger)

Таймери

Besides state timeouts, FSM manages timers identified by String names. You may set a timer using

setTimer(name, msg, interval, repeat)

where msg is the message object which will be sent after the duration interval has elapsed. If repeat is true, then the timer is scheduled at fixed rate given by the interval parameter. Any existing timer with the same name will automatically be canceled before adding the new timer.

Timers may be canceled using

cancelTimer(name)

which is guaranteed to work immediately, meaning that the scheduled message will not be processed after this call even if the timer already fired and queued it. The status of any timer may be inquired with

isTimerActive(name)

These named timers complement state timeouts because they are not affected by intervening reception of other messages.

Termination from Inside

The FSM is stopped by specifying the result state as

stop([reason[, data]])

The reason must be one of Normal (which is the default), Shutdown or Failure(reason), and the second argument may be given to change the state data which is available during termination handling.

Note

It should be noted that stop does not abort the actions and stop the FSM immediately. The stop action must be returned from the event handler in the same way as a state transition (but note that the return statement may not be used within a when block).

  1. when(Error) {
  2. case Event("stop", _) =>
  3. // do cleanup ...
  4. stop()
  5. }

You can use onTermination(handler) to specify custom code that is executed when the FSM is stopped. The handler is a partial function which takes a StopEvent(reason, stateName, stateData) as argument:

  1. onTermination {
  2. case StopEvent(FSM.Normal, state, data) => // ...
  3. case StopEvent(FSM.Shutdown, state, data) => // ...
  4. case StopEvent(FSM.Failure(cause), state, data) => // ...
  5. }

As for the whenUnhandled case, this handler is not stacked, so each invocation of onTermination replaces the previously installed handler.

Termination from Outside

When an ActorRef associated to a FSM is stopped using the stop method, its postStop hook will be executed. The default implementation by the FSM trait is to execute the onTermination handler if that is prepared to handle aStopEvent(Shutdown, ...).

Warning

In case you override postStop and want to have your onTermination handler called, do not forget to callsuper.postStop.

Testing and Debugging Finite State Machines

During development and for trouble shooting FSMs need care just as any other actor. There are specialized tools available as described in Testing Finite State Machines and in the following.

Event Tracing

The setting akka.actor.debug.fsm in Configuration enables logging of an event trace by LoggingFSM instances:

  1. import akka.actor.LoggingFSM
  2. class MyFSM extends LoggingFSM[StateType, Data] {
  3. // body elided ...
  4. }

This FSM will log at DEBUG level:

  • all processed events, including StateTimeout and scheduled timer messages
  • every setting and cancellation of named timers
  • all state transitions

Life cycle changes and special messages can be logged as described for Actors.

Rolling Event Log

The LoggingFSM trait adds one more feature to the FSM: a rolling event log which may be used during debugging (for tracing how the FSM entered a certain failure state) or for other creative uses:

  1. import akka.actor.LoggingFSM
  2. class MyFSM extends LoggingFSM[StateType, Data] {
  3. override def logDepth = 12
  4. onTermination {
  5. case StopEvent(FSM.Failure(_), state, data) =>
  6. val lastEvents = getLog.mkString("\n\t")
  7. log.warning("Failure in state " + state + " with data " + data + "\n" +
  8. "Events leading up to this point:\n\t" + lastEvents)
  9. }
  10. // ...
  11. }

The logDepth defaults to zero, which turns off the event log.

Warning

The log buffer is allocated during actor creation, which is why the configuration is done using a virtual method call. If you want to override with a val, make sure that its initialization happens before the initializer of LoggingFSMruns, and do not change the value returned by logDepth after the buffer has been allocated.

The contents of the event log are available using method getLog, which returns an IndexedSeq[LogEntry] where the oldest entry is at index zero.

Examples

A bigger FSM example contrasted with Actor's become/unbecome can be found in the Lightbend Activator template named Akka FSM in Scala

Persistence

Akka persistence enables stateful actors to persist their internal state so that it can be recovered when an actor is started, restarted after a JVM crash or by a supervisor, or migrated in a cluster. The key concept behind Akka persistence is that only changes to an actor's internal state are persisted but never its current state directly (except for optional snapshots). These changes are only ever appended to storage, nothing is ever mutated, which allows for very high transaction rates and efficient replication. Stateful actors are recovered by replaying stored changes to these actors from which they can rebuild internal state. This can be either the full history of changes or starting from a snapshot which can dramatically reduce recovery times. Akka persistence also provides point-to-point communication with at-least-once message delivery semantics.

Akka persistence is inspired by and the official replacement of the eventsourced library. It follows the same concepts and architecture of eventsourced but significantly differs on API and implementation level. See also Migration Guide Eventsourced to Akka Persistence 2.3.x

Dependencies

Akka persistence is a separate jar file. Make sure that you have the following dependency in your project:

  1. "com.typesafe.akka" %% "akka-persistence" % "2.4.9"

The Akka persistence extension comes with few built-in persistence plugins, including in-memory heap based journal, local file-system based snapshot-store and LevelDB based journal.

LevelDB based plugins will require the following additional dependency declaration:

  1. "org.iq80.leveldb" % "leveldb" % "0.7"
  2. "org.fusesource.leveldbjni" % "leveldbjni-all" % "1.8"

Architecture

Event sourcing

The basic idea behind Event Sourcing is quite simple. A persistent actor receives a (non-persistent) command which is first validated if it can be applied to the current state. Here validation can mean anything, from simple inspection of a command message's fields up to a conversation with several external services, for example. If validation succeeds, events are generated from the command, representing the effect of the command. These events are then persisted and, after successful persistence, used to change the actor's state. When the persistent actor needs to be recovered, only the persisted events are replayed of which we know that they can be successfully applied. In other words, events cannot fail when being replayed to a persistent actor, in contrast to commands. Event sourced actors may of course also process commands that do not change application state such as query commands for example.

Akka persistence supports event sourcing with the PersistentActor trait. An actor that extends this trait uses thepersist method to persist and handle events. The behavior of a PersistentActor is defined by implementingreceiveRecover and receiveCommand. This is demonstrated in the following example.

  1. import akka.actor._
  2. import akka.persistence._
  3.  
  4. case class Cmd(data: String)
  5. case class Evt(data: String)
  6.  
  7. case class ExampleState(events: List[String] = Nil) {
  8. def updated(evt: Evt): ExampleState = copy(evt.data :: events)
  9. def size: Int = events.length
  10. override def toString: String = events.reverse.toString
  11. }
  12.  
  13. class ExamplePersistentActor extends PersistentActor {
  14. override def persistenceId = "sample-id-1"
  15.  
  16. var state = ExampleState()
  17.  
  18. def updateState(event: Evt): Unit =
  19. state = state.updated(event)
  20.  
  21. def numEvents =
  22. state.size
  23.  
  24. val receiveRecover: Receive = {
  25. case evt: Evt => updateState(evt)
  26. case SnapshotOffer(_, snapshot: ExampleState) => state = snapshot
  27. }
  28.  
  29. val receiveCommand: Receive = {
  30. case Cmd(data) =>
  31. persist(Evt(s"${data}-${numEvents}"))(updateState)
  32. persist(Evt(s"${data}-${numEvents + 1}")) { event =>
  33. updateState(event)
  34. context.system.eventStream.publish(event)
  35. }
  36. case "snap" => saveSnapshot(state)
  37. case "print" => println(state)
  38. }
  39.  
  40. }

The example defines two data types, Cmd and Evt to represent commands and events, respectively. The state of the ExamplePersistentActor is a list of persisted event data contained in ExampleState.

The persistent actor's receiveRecover method defines how state is updated during recovery by handling Evtand SnapshotOffer messages. The persistent actor's receiveCommand method is a command handler. In this example, a command is handled by generating two events which are then persisted and handled. Events are persisted by calling persist with an event (or a sequence of events) as first argument and an event handler as second argument.

The persist method persists events asynchronously and the event handler is executed for successfully persisted events. Successfully persisted events are internally sent back to the persistent actor as individual messages that trigger event handler executions. An event handler may close over persistent actor state and mutate it. The sender of a persisted event is the sender of the corresponding command. This allows event handlers to reply to the sender of a command (not shown).

The main responsibility of an event handler is changing persistent actor state using event data and notifying others about successful state changes by publishing events.

When persisting events with persist it is guaranteed that the persistent actor will not receive further commands between the persist call and the execution(s) of the associated event handler. This also holds for multiple persistcalls in context of a single command. Incoming messages are stashed until the persist is completed.

If persistence of an event fails, onPersistFailure will be invoked (logging the error by default), and the actor will unconditionally be stopped. If persistence of an event is rejected before it is stored, e.g. due to serialization error,onPersistRejected will be invoked (logging a warning by default) and the actor continues with the next message.

The easiest way to run this example yourself is to download Lightbend Activator and open the tutorial named Akka Persistence Samples with Scala. It contains instructions on how to run the PersistentActorExample.

Note

It's also possible to switch between different command handlers during normal processing and recovery withcontext.become() and context.unbecome(). To get the actor into the same state after recovery you need to take special care to perform the same state transitions with become and unbecome in the receiveRecovermethod as you would have done in the command handler. Note that when using become from receiveRecoverit will still only use the receiveRecover behavior when replaying the events. When replay is completed it will use the new behavior.

Identifiers

A persistent actor must have an identifier that doesn't change across different actor incarnations. The identifier must be defined with the persistenceId method.

  1. override def persistenceId = "my-stable-persistence-id"

Recovery

By default, a persistent actor is automatically recovered on start and on restart by replaying journaled messages. New messages sent to a persistent actor during recovery do not interfere with replayed messages. They are cached and received by a persistent actor after recovery phase completes.

Note

Accessing the sender() for replayed messages will always result in a deadLetters reference, as the original sender is presumed to be long gone. If you indeed have to notify an actor during recovery in the future, store itsActorPath explicitly in your persisted events.

Recovery customization

Applications may also customise how recovery is performed by returning a customised Recovery object in therecovery method of a PersistentActor, for example setting an upper bound to the replay which allows the actor to be replayed to a certain point "in the past" instead to its most up to date state:

  1. override def recovery = Recovery(toSequenceNr = 457L)

Recovery can be disabled by returning Recovery.none() in the recovery method of a PersistentActor:

  1. override def recovery = Recovery.none

Recovery status

A persistent actor can query its own recovery status via the methods

  1. def recoveryRunning: Boolean
  2. def recoveryFinished: Boolean

Sometimes there is a need for performing additional initialization when the recovery has completed before processing any other message sent to the persistent actor. The persistent actor will receive a special RecoveryCompletedmessage right after recovery and before any other received messages.

  1. override def receiveRecover: Receive = {
  2. case RecoveryCompleted =>
  3. // perform init after recovery, before any other messages
  4. //...
  5. case evt => //...
  6. }
  7.  
  8. override def receiveCommand: Receive = {
  9. case msg => //...
  10. }

If there is a problem with recovering the state of the actor from the journal, onRecoveryFailure is called (logging the error by default) and the actor will be stopped.

Internal stash

The persistent actor has a private stash for internally caching incoming messages during recovery or thepersist\persistAll method persisting events. You can still use/inherit from the Stash interface. The internal stash cooperates with the normal stash by hooking into unstashAll method and making sure messages are unstashed properly to the internal stash to maintain ordering guarantees.

You should be careful to not send more messages to a persistent actor than it can keep up with, otherwise the number of stashed messages will grow without bounds. It can be wise to protect against OutOfMemoryError by defining a maximum stash capacity in the mailbox configuration:

  1. akka.actor.default-mailbox.stash-capacity=10000

Note that the stash capacity is per actor. If you have many persistent actors, e.g. when using cluster sharding, you may need to define a small stash capacity to ensure that the total number of stashed messages in the system don't consume too much memory. Additionally, The persistent actor defines three strategies to handle failure when the internal stash capacity is exceeded. The default overflow strategy is the ThrowOverflowExceptionStrategy, which discards the current received message and throws a StashOverflowException, causing actor restart if default supervision strategy is used. you can override the internalStashOverflowStrategy method to returnDiscardToDeadLetterStrategy or ReplyToStrategy for any "individual" persistent actor, or define the "default" for all persistent actors by providing FQCN, which must be a subclass of StashOverflowStrategyConfigurator, in the persistence configuration:

  1. akka.persistence.internal-stash-overflow-strategy=
  2. "akka.persistence.ThrowExceptionConfigurator"

The DiscardToDeadLetterStrategy strategy also has a pre-packaged companion configuratorakka.persistence.DiscardConfigurator.

You can also query default strategy via the Akka persistence extension singleton:

  1. Persistence(context.system).defaultInternalStashOverflowStrategy

Note

The bounded mailbox should be avoided in the persistent actor, by which the messages come from storage backends may be discarded. You can use bounded stash instead of it.

Relaxed local consistency requirements and high throughput use-cases

If faced with relaxed local consistency requirements and high throughput demands sometimes PersistentActor and its persist may not be enough in terms of consuming incoming Commands at a high rate, because it has to wait until all Events related to a given Command are processed in order to start processing the next Command. While this abstraction is very useful for most cases, sometimes you may be faced with relaxed requirements about consistency – for example you may want to process commands as fast as you can, assuming that the Event will eventually be persisted and handled properly in the background, retroactively reacting to persistence failures if needed.

The persistAsync method provides a tool for implementing high-throughput persistent actors. It will not stash incoming Commands while the Journal is still working on persisting and/or user code is executing event callbacks.

In the below example, the event callbacks may be called "at any time", even after the next Command has been processed. The ordering between events is still guaranteed ("evt-b-1" will be sent after "evt-a-2", which will be sent after "evt-a-1" etc.).

  1. class MyPersistentActor extends PersistentActor {
  2.  
  3. override def persistenceId = "my-stable-persistence-id"
  4.  
  5. override def receiveRecover: Receive = {
  6. case _ => // handle recovery here
  7. }
  8.  
  9. override def receiveCommand: Receive = {
  10. case c: String => {
  11. sender() ! c
  12. persistAsync(s"evt-$c-1") { e => sender() ! e }
  13. persistAsync(s"evt-$c-2") { e => sender() ! e }
  14. }
  15. }
  16. }
  17.  
  18. // usage
  19. persistentActor ! "a"
  20. persistentActor ! "b"
  21.  
  22. // possible order of received messages:
  23. // a
  24. // b
  25. // evt-a-1
  26. // evt-a-2
  27. // evt-b-1
  28. // evt-b-2

Note

In order to implement the pattern known as "command sourcing" simply call persistAsync(cmd)(...) right away on all incoming messages and handle them in the callback.

Warning

The callback will not be invoked if the actor is restarted (or stopped) in between the call to persistAsync and the journal has confirmed the write.

Deferring actions until preceding persist handlers have executed

Sometimes when working with persistAsync you may find that it would be nice to define some actions in terms of ''happens-after the previous persistAsync handlers have been invoked''. PersistentActor provides an utility method called deferAsync, which works similarly to persistAsync yet does not persist the passed in event. It is recommended to use it for read operations, and actions which do not have corresponding events in your domain model.

Using this method is very similar to the persist family of methods, yet it does not persist the passed in event. It will be kept in memory and used when invoking the handler.

  1. class MyPersistentActor extends PersistentActor {
  2.  
  3. override def persistenceId = "my-stable-persistence-id"
  4.  
  5. override def receiveRecover: Receive = {
  6. case _ => // handle recovery here
  7. }
  8.  
  9. override def receiveCommand: Receive = {
  10. case c: String => {
  11. sender() ! c
  12. persistAsync(s"evt-$c-1") { e => sender() ! e }
  13. persistAsync(s"evt-$c-2") { e => sender() ! e }
  14. deferAsync(s"evt-$c-3") { e => sender() ! e }
  15. }
  16. }
  17. }

Notice that the sender() is safe to access in the handler callback, and will be pointing to the original sender of the command for which this deferAsync handler was called.

The calling side will get the responses in this (guaranteed) order:

  1. persistentActor ! "a"
  2. persistentActor ! "b"
  3.  
  4. // order of received messages:
  5. // a
  6. // b
  7. // evt-a-1
  8. // evt-a-2
  9. // evt-a-3
  10. // evt-b-1
  11. // evt-b-2
  12. // evt-b-3

Warning

The callback will not be invoked if the actor is restarted (or stopped) in between the call to deferAsync and the journal has processed and confirmed all preceding writes.

Nested persist calls

It is possible to call persist and persistAsync inside their respective callback blocks and they will properly retain both the thread safety (including the right value of sender()) as well as stashing guarantees.

In general it is encouraged to create command handlers which do not need to resort to nested event persisting, however there are situations where it may be useful. It is important to understand the ordering of callback execution in those situations, as well as their implication on the stashing behaviour (that persist() enforces). In the following example two persist calls are issued, and each of them issues another persist inside its callback:

  1. override def receiveCommand: Receive = {
  2. case c: String =>
  3. sender() ! c
  4.  
  5. persist(s"$c-1-outer") { outer1 =>
  6. sender() ! outer1
  7. persist(s"$c-1-inner") { inner1 =>
  8. sender() ! inner1
  9. }
  10. }
  11.  
  12. persist(s"$c-2-outer") { outer2 =>
  13. sender() ! outer2
  14. persist(s"$c-2-inner") { inner2 =>
  15. sender() ! inner2
  16. }
  17. }
  18. }

When sending two commands to this PersistentActor, the persist handlers will be executed in the following order:

  1. persistentActor ! "a"
  2. persistentActor ! "b"
  3.  
  4. // order of received messages:
  5. // a
  6. // a-outer-1
  7. // a-outer-2
  8. // a-inner-1
  9. // a-inner-2
  10. // and only then process "b"
  11. // b
  12. // b-outer-1
  13. // b-outer-2
  14. // b-inner-1
  15. // b-inner-2

First the "outer layer" of persist calls is issued and their callbacks are applied. After these have successfully completed, the inner callbacks will be invoked (once the events they are persisting have been confirmed to be persisted by the journal). Only after all these handlers have been successfully invoked will the next command be delivered to the persistent Actor. In other words, the stashing of incoming commands that is guaranteed by initially calling persist()on the outer layer is extended until all nested persist callbacks have been handled.

It is also possible to nest persistAsync calls, using the same pattern:

  1. override def receiveCommand: Receive = {
  2. case c: String =>
  3. sender() ! c
  4. persistAsync(c + "-outer-1") { outer =>
  5. sender() ! outer
  6. persistAsync(c + "-inner-1") { inner => sender() ! inner }
  7. }
  8. persistAsync(c + "-outer-2") { outer =>
  9. sender() ! outer
  10. persistAsync(c + "-inner-2") { inner => sender() ! inner }
  11. }
  12. }

In this case no stashing is happening, yet events are still persisted and callbacks are executed in the expected order:

  1. persistentActor ! "a"
  2. persistentActor ! "b"
  3.  
  4. // order of received messages:
  5. // a
  6. // b
  7. // a-outer-1
  8. // a-outer-2
  9. // b-outer-1
  10. // b-outer-2
  11. // a-inner-1
  12. // a-inner-2
  13. // b-inner-1
  14. // b-inner-2
  15.  
  16. // which can be seen as the following causal relationship:
  17. // a -> a-outer-1 -> a-outer-2 -> a-inner-1 -> a-inner-2
  18. // b -> b-outer-1 -> b-outer-2 -> b-inner-1 -> b-inner-2

While it is possible to nest mixed persist and persistAsync with keeping their respective semantics it is not a recommended practice, as it may lead to overly complex nesting.

Failures

If persistence of an event fails, onPersistFailure will be invoked (logging the error by default), and the actor will unconditionally be stopped.

The reason that it cannot resume when persist fails is that it is unknown if the event was actually persisted or not, and therefore it is in an inconsistent state. Restarting on persistent failures will most likely fail anyway since the journal is probably unavailable. It is better to stop the actor and after a back-off timeout start it again. Theakka.pattern.BackoffSupervisor actor is provided to support such restarts.

  1. val childProps = Props[MyPersistentActor]
  2. val props = BackoffSupervisor.props(
  3. Backoff.onStop(
  4. childProps,
  5. childName = "myActor",
  6. minBackoff = 3.seconds,
  7. maxBackoff = 30.seconds,
  8. randomFactor = 0.2))
  9. context.actorOf(props, name = "mySupervisor")

If persistence of an event is rejected before it is stored, e.g. due to serialization error, onPersistRejected will be invoked (logging a warning by default), and the actor continues with next message.

If there is a problem with recovering the state of the actor from the journal when the actor is started,onRecoveryFailure is called (logging the error by default), and the actor will be stopped.

Atomic writes

Each event is of course stored atomically, but it is also possible to store several events atomically by using thepersistAll or persistAllAsync method. That means that all events passed to that method are stored or none of them are stored if there is an error.

The recovery of a persistent actor will therefore never be done partially with only a subset of events persisted bypersistAll.

Some journals may not support atomic writes of several events and they will then reject the persistAll command, i.e. onPersistRejected is called with an exception (typically UnsupportedOperationException).

Batch writes

In order to optimize throughput when using persistAsync, a persistent actor internally batches events to be stored under high load before writing them to the journal (as a single batch). The batch size is dynamically determined by how many events are emitted during the time of a journal round-trip: after sending a batch to the journal no further batch can be sent before confirmation has been received that the previous batch has been written. Batch writes are never timer-based which keeps latencies at a minimum.

Message deletion

It is possible to delete all messages (journaled by a single persistent actor) up to a specified sequence number; Persistent actors may call the deleteMessages method to this end.

Deleting messages in event sourcing based applications is typically either not used at all, or used in conjunction withsnapshotting, i.e. after a snapshot has been successfully stored, a deleteMessages(toSequenceNr) up until the sequence number of the data held by that snapshot can be issued to safely delete the previous events while still having access to the accumulated state during replays - by loading the snapshot.

The result of the deleteMessages request is signaled to the persistent actor with a DeleteMessagesSuccessmessage if the delete was successful or a DeleteMessagesFailure message if it failed.

Message deletion doesn't affect the highest sequence number of the journal, even if all messages were deleted from it after deleteMessages invocation.

Persistence status handling

Persisting, deleting, and replaying messages can either succeed or fail.

MethodSuccessFailure / RejectionAfter failure handler invoked
persist / persistAsyncpersist handler invokedonPersistFailureActor is stopped.
onPersistRejectedNo automatic actions.
recoveryRecoveryCompletedonRecoveryFailureActor is stopped.
deleteMessagesDeleteMessagesSuccessDeleteMessagesFailureNo automatic actions.

The most important operations (persist and recovery) have failure handlers modelled as explicit callbacks which the user can override in the PersistentActor. The default implementations of these handlers emit a log message (error for persist/recovery failures, and warning for others), logging the failure cause and information about which message caused the failure.

For critical failures, such as recovery or persisting events failing, the persistent actor will be stopped after the failure handler is invoked. This is because if the underlying journal implementation is signalling persistence failures it is most likely either failing completely or overloaded and restarting right-away and trying to persist the event again will most likely not help the journal recover – as it would likely cause a Thundering herd problem, as many persistent actors would restart and try to persist their events again. Instead, using a BackoffSupervisor (as described in Failures) which implements an exponential-backoff strategy which allows for more breathing room for the journal to recover between restarts of the persistent actor.

Note

Journal implementations may choose to implement a retry mechanism, e.g. such that only after a write fails N number of times a persistence failure is signalled back to the user. In other words, once a journal returns a failure, it is considered fatal by Akka Persistence, and the persistent actor which caused the failure will be stopped.

Check the documentation of the journal implementation you are using for details if/how it is using this technique.

Safely shutting down persistent actors

Special care should be given when shutting down persistent actors from the outside. With normal Actors it is often acceptable to use the special PoisonPill message to signal to an Actor that it should stop itself once it receives this message – in fact this message is handled automatically by Akka, leaving the target actor no way to refuse stopping itself when given a poison pill.

This can be dangerous when used with PersistentActor due to the fact that incoming commands are stashed while the persistent actor is awaiting confirmation from the Journal that events have been written when persist() was used. Since the incoming commands will be drained from the Actor's mailbox and put into its internal stash while awaiting the confirmation (thus, before calling the persist handlers) the Actor may receive and (auto)handle the PoisonPill before it processes the other messages which have been put into its stash, causing a pre-mature shutdown of the Actor.

Warning

Consider using explicit shut-down messages instead of PoisonPill when working with persistent actors.

The example below highlights how messages arrive in the Actor's mailbox and how they interact with its internal stashing mechanism when persist() is used. Notice the early stop behaviour that occurs when PoisonPill is used:

  1. /** Explicit shutdown message */
  2. case object Shutdown
  3.  
  4. class SafePersistentActor extends PersistentActor {
  5. override def persistenceId = "safe-actor"
  6.  
  7. override def receiveCommand: Receive = {
  8. case c: String =>
  9. println(c)
  10. persist(s"handle-$c") { println(_) }
  11. case Shutdown =>
  12. context.stop(self)
  13. }
  14.  
  15. override def receiveRecover: Receive = {
  16. case _ => // handle recovery here
  17. }
  18. }
  1. // UN-SAFE, due to PersistentActor's command stashing:
  2. persistentActor ! "a"
  3. persistentActor ! "b"
  4. persistentActor ! PoisonPill
  5. // order of received messages:
  6. // a
  7. // # b arrives at mailbox, stashing; internal-stash = [b]
  8. // PoisonPill is an AutoReceivedMessage, is handled automatically
  9. // !! stop !!
  10. // Actor is stopped without handling `b` nor the `a` handler!
  1. // SAFE:
  2. persistentActor ! "a"
  3. persistentActor ! "b"
  4. persistentActor ! Shutdown
  5. // order of received messages:
  6. // a
  7. // # b arrives at mailbox, stashing; internal-stash = [b]
  8. // # Shutdown arrives at mailbox, stashing; internal-stash = [b, Shutdown]
  9. // handle-a
  10. // # unstashing; internal-stash = [Shutdown]
  11. // b
  12. // handle-b
  13. // # unstashing; internal-stash = []
  14. // Shutdown
  15. // -- stop --

Persistent Views

Warning

PersistentView is deprecated. Use Persistence Query instead. The corresponding query type isEventsByPersistenceId. There are several alternatives for connecting the Source to an actor corresponding to a previous PersistentView actor:

The consuming actor may be a plain Actor or a PersistentActor if it needs to store its own state (e.g. fromSequenceNr offset).

Persistent views can be implemented by extending the PersistentView trait and implementing the receive and the persistenceId methods.

  1. class MyView extends PersistentView {
  2. override def persistenceId: String = "some-persistence-id"
  3. override def viewId: String = "some-persistence-id-view"
  4.  
  5. def receive: Receive = {
  6. case payload if isPersistent =>
  7. // handle message from journal...
  8. case payload =>
  9. // handle message from user-land...
  10. }
  11. }

The persistenceId identifies the persistent actor from which the view receives journaled messages. It is not necessary that the referenced persistent actor is actually running. Views read messages from a persistent actor's journal directly. When a persistent actor is started later and begins to write new messages, by default the corresponding view is updated automatically.

It is possible to determine if a message was sent from the Journal or from another actor in user-land by calling theisPersistent method. Having that said, very often you don't need this information at all and can simply apply the same logic to both cases (skip the if isPersistent check).

Updates

The default update interval of all views of an actor system is configurable:

  1. akka.persistence.view.auto-update-interval = 5s

PersistentView implementation classes may also override the autoUpdateInterval method to return a custom update interval for a specific view class or view instance. Applications may also trigger additional updates at any time by sending a view an Update message.

  1. val view = system.actorOf(Props[MyView])
  2. view ! Update(await = true)

If the await parameter is set to true, messages that follow the Update request are processed when the incremental message replay, triggered by that update request, completed. If set to false (default), messages following the update request may interleave with the replayed message stream. Automated updates always run with await =false.

Automated updates of all persistent views of an actor system can be turned off by configuration:

  1. akka.persistence.view.auto-update = off

Implementation classes may override the configured default value by overriding the autoUpdate method. To limit the number of replayed messages per update request, applications can configure a customakka.persistence.view.auto-update-replay-max value or override the autoUpdateReplayMax method. The number of replayed messages for manual updates can be limited with the replayMax parameter of the Updatemessage.

Recovery

Initial recovery of persistent views works the very same way as for persistent actors (i.e. by sending a Recovermessage to self). The maximum number of replayed messages during initial recovery is determined byautoUpdateReplayMax. Further possibilities to customize initial recovery are explained in section Recovery.

Identifiers

A persistent view must have an identifier that doesn't change across different actor incarnations. The identifier must be defined with the viewId method.

The viewId must differ from the referenced persistenceId, unless Snapshots of a view and its persistent actor should be shared (which is what applications usually do not want).

Snapshots

Snapshots can dramatically reduce recovery times of persistent actors and views. The following discusses snapshots in context of persistent actors but this is also applicable to persistent views.

Persistent actors can save snapshots of internal state by calling the saveSnapshot method. If saving of a snapshot succeeds, the persistent actor receives a SaveSnapshotSuccess message, otherwise a SaveSnapshotFailuremessage

  1. var state: Any = _
  2.  
  3. override def receiveCommand: Receive = {
  4. case "snap" => saveSnapshot(state)
  5. case SaveSnapshotSuccess(metadata) => // ...
  6. case SaveSnapshotFailure(metadata, reason) => // ...
  7. }

where metadata is of type SnapshotMetadata:

  1. final case class SnapshotMetadata(persistenceId: String, sequenceNr: Long, timestamp: Long = 0L)

During recovery, the persistent actor is offered a previously saved snapshot via a SnapshotOffer message from which it can initialize internal state.

  1. var state: Any = _
  2.  
  3. override def receiveRecover: Receive = {
  4. case SnapshotOffer(metadata, offeredSnapshot) => state = offeredSnapshot
  5. case RecoveryCompleted =>
  6. case event => // ...
  7. }

The replayed messages that follow the SnapshotOffer message, if any, are younger than the offered snapshot. They finally recover the persistent actor to its current (i.e. latest) state.

In general, a persistent actor is only offered a snapshot if that persistent actor has previously saved one or more snapshots and at least one of these snapshots matches the SnapshotSelectionCriteria that can be specified for recovery.

  1. override def recovery = Recovery(fromSnapshot = SnapshotSelectionCriteria(
  2. maxSequenceNr = 457L,
  3. maxTimestamp = System.currentTimeMillis))

If not specified, they default to SnapshotSelectionCriteria.Latest which selects the latest (= youngest) snapshot. To disable snapshot-based recovery, applications should use SnapshotSelectionCriteria.None. A recovery where no saved snapshot matches the specified SnapshotSelectionCriteria will replay all journaled messages.

Note

In order to use snapshots, a default snapshot-store (akka.persistence.snapshot-store.plugin) must be configured, or the PersistentActor can pick a snapshot store explicitly by overriding defsnapshotPluginId: String.

Since it is acceptable for some applications to not use any snapshotting, it is legal to not configure a snapshot store. However, Akka will log a warning message when this situation is detected and then continue to operate until an actor tries to store a snapshot, at which point the operation will fail (by replying with anSaveSnapshotFailure for example).

Note that Cluster Sharding is using snapshots, so if you use Cluster Sharding you need to define a snapshot store plugin.

Snapshot deletion

A persistent actor can delete individual snapshots by calling the deleteSnapshot method with the sequence number of when the snapshot was taken.

To bulk-delete a range of snapshots matching SnapshotSelectionCriteria, persistent actors should use thedeleteSnapshots method.

Snapshot status handling

Saving or deleting snapshots can either succeed or fail – this information is reported back to the persistent actor via status messages as illustrated in the following table.

MethodSuccessFailure message
saveSnapshot(Any)SaveSnapshotSuccessSaveSnapshotFailure
deleteSnapshot(Long)DeleteSnapshotSuccessDeleteSnapshotFailure
deleteSnapshots(SnapshotSelectionCriteria)DeleteSnapshotsSuccessDeleteSnapshotsFailure

If failure messages are left unhandled by the actor, a default warning log message will be logged for each incoming failure message. No default action is performed on the success messages, however you're free to handle them e.g. in order to delete an in memory representation of the snapshot, or in the case of failure to attempt save the snapshot again.

At-Least-Once Delivery

To send messages with at-least-once delivery semantics to destinations you can mix-in AtLeastOnceDelivery trait to your PersistentActor on the sending side. It takes care of re-sending messages when they have not been confirmed within a configurable timeout.

The state of the sending actor, including which messages have been sent that have not been confirmed by the recipient must be persistent so that it can survive a crash of the sending actor or JVM. The AtLeastOnceDelivery trait does not persist anything by itself. It is your responsibility to persist the intent that a message is sent and that a confirmation has been received.

Note

At-least-once delivery implies that original message sending order is not always preserved, and the destination may receive duplicate messages. Semantics do not match those of a normal ActorRef send operation:

These semantics are similar to what an ActorPath represents (see Actor Lifecycle), therefore you need to supply a path and not a reference when delivering messages. The messages are sent to the path with an actor selection.

Use the deliver method to send a message to a destination. Call the confirmDelivery method when the destination has replied with a confirmation message.

Relationship between deliver and confirmDelivery

To send messages to the destination path, use the deliver method after you have persisted the intent to send the message.

The destination actor must send back a confirmation message. When the sending actor receives this confirmation message you should persist the fact that the message was delivered successfully and then call the confirmDeliverymethod.

If the persistent actor is not currently recovering, the deliver method will send the message to the destination actor. When recovering, messages will be buffered until they have been confirmed using confirmDelivery. Once recovery has completed, if there are outstanding messages that have not been confirmed (during the message replay), the persistent actor will resend these before sending any other messages.

Deliver requires a deliveryIdToMessage function to pass the provided deliveryId into the message so that the correlation between deliver and confirmDelivery is possible. The deliveryId must do the round trip. Upon receipt of the message, the destination actor will send the same``deliveryId`` wrapped in a confirmation message back to the sender. The sender will then use it to call confirmDelivery method to complete the delivery routine.

  1. import akka.actor.{ Actor, ActorSelection }
  2. import akka.persistence.AtLeastOnceDelivery
  3.  
  4. case class Msg(deliveryId: Long, s: String)
  5. case class Confirm(deliveryId: Long)
  6.  
  7. sealed trait Evt
  8. case class MsgSent(s: String) extends Evt
  9. case class MsgConfirmed(deliveryId: Long) extends Evt
  10.  
  11. class MyPersistentActor(destination: ActorSelection)
  12. extends PersistentActor with AtLeastOnceDelivery {
  13.  
  14. override def persistenceId: String = "persistence-id"
  15.  
  16. override def receiveCommand: Receive = {
  17. case s: String => persist(MsgSent(s))(updateState)
  18. case Confirm(deliveryId) => persist(MsgConfirmed(deliveryId))(updateState)
  19. }
  20.  
  21. override def receiveRecover: Receive = {
  22. case evt: Evt => updateState(evt)
  23. }
  24.  
  25. def updateState(evt: Evt): Unit = evt match {
  26. case MsgSent(s) =>
  27. deliver(destination)(deliveryId => Msg(deliveryId, s))
  28.  
  29. case MsgConfirmed(deliveryId) => confirmDelivery(deliveryId)
  30. }
  31. }
  32.  
  33. class MyDestination extends Actor {
  34. def receive = {
  35. case Msg(deliveryId, s) =>
  36. // ...
  37. sender() ! Confirm(deliveryId)
  38. }
  39. }

The deliveryId generated by the persistence module is a strictly monotonically increasing sequence number without gaps. The same sequence is used for all destinations of the actor, i.e. when sending to multiple destinations the destinations will see gaps in the sequence. It is not possible to use custom deliveryId. However, you can send a custom correlation identifier in the message to the destination. You must then retain a mapping between the internaldeliveryId (passed into the deliveryIdToMessage function) and your custom correlation id (passed into the message). You can do this by storing such mapping in a Map(correlationId -> deliveryId) from which you can retrieve the deliveryId to be passed into the confirmDelivery method once the receiver of your message has replied with your custom correlation id.

The AtLeastOnceDelivery trait has a state consisting of unconfirmed messages and a sequence number. It does not store this state itself. You must persist events corresponding to the deliver and confirmDelivery invocations from your PersistentActor so that the state can be restored by calling the same methods during the recovery phase of thePersistentActor. Sometimes these events can be derived from other business level events, and sometimes you must create separate events. During recovery, calls to deliver will not send out messages, those will be sent later if no matching confirmDelivery will have been performed.

Support for snapshots is provided by getDeliverySnapshot and setDeliverySnapshot. TheAtLeastOnceDeliverySnapshot contains the full delivery state, including unconfirmed messages. If you need a custom snapshot for other parts of the actor state you must also include the AtLeastOnceDeliverySnapshot. It is serialized using protobuf with the ordinary Akka serialization mechanism. It is easiest to include the bytes of theAtLeastOnceDeliverySnapshot as a blob in your custom snapshot.

The interval between redelivery attempts is defined by the redeliverInterval method. The default value can be configured with the akka.persistence.at-least-once-delivery.redeliver-interval configuration key. The method can be overridden by implementation classes to return non-default values.

The maximum number of messages that will be sent at each redelivery burst is defined by the redeliveryBurstLimitmethod (burst frequency is half of the redelivery interval). If there's a lot of unconfirmed messages (e.g. if the destination is not available for a long time), this helps to prevent an overwhelming amount of messages to be sent at once. The default value can be configured with the akka.persistence.at-least-once-delivery.redelivery-burst-limit configuration key. The method can be overridden by implementation classes to return non-default values.

After a number of delivery attempts a AtLeastOnceDelivery.UnconfirmedWarning message will be sent to self. The re-sending will still continue, but you can choose to call confirmDelivery to cancel the re-sending. The number of delivery attempts before emitting the warning is defined by the warnAfterNumberOfUnconfirmedAttemptsmethod. The default value can be configured with the akka.persistence.at-least-once-delivery.warn-after-number-of-unconfirmed-attempts configuration key. The method can be overridden by implementation classes to return non-default values.

The AtLeastOnceDelivery trait holds messages in memory until their successful delivery has been confirmed. The maximum number of unconfirmed messages that the actor is allowed to hold in memory is defined by themaxUnconfirmedMessages method. If this limit is exceed the deliver method will not accept more messages and it will throw AtLeastOnceDelivery.MaxUnconfirmedMessagesExceededException. The default value can be configured with the akka.persistence.at-least-once-delivery.max-unconfirmed-messages configuration key. The method can be overridden by implementation classes to return non-default values.

Event Adapters

In long running projects using event sourcing sometimes the need arises to detach the data model from the domain model completely.

Event Adapters help in situations where:

Implementing an EventAdapter is rather stright forward:

  1. class MyEventAdapter(system: ExtendedActorSystem) extends EventAdapter {
  2. override def manifest(event: Any): String =
  3. "" // when no manifest needed, return ""
  4.  
  5. override def toJournal(event: Any): Any =
  6. event // identity
  7.  
  8. override def fromJournal(event: Any, manifest: String): EventSeq =
  9. EventSeq.single(event) // identity
  10. }

Then in order for it to be used on events coming to and from the journal you must bind it using the below configuration syntax:

  1. akka.persistence.journal {
  2. inmem {
  3. event-adapters {
  4. tagging = "docs.persistence.MyTaggingEventAdapter"
  5. user-upcasting = "docs.persistence.UserUpcastingEventAdapter"
  6. item-upcasting = "docs.persistence.ItemUpcastingEventAdapter"
  7. }
  8.  
  9. event-adapter-bindings {
  10. "docs.persistence.Item" = tagging
  11. "docs.persistence.TaggedEvent" = tagging
  12. "docs.persistence.v1.Event" = [user-upcasting, item-upcasting]
  13. }
  14. }
  15. }

It is possible to bind multiple adapters to one class for recovery, in which case the fromJournal methods of all bound adapters will be applied to a given matching event (in order of definition in the configuration). Since each adapter may return from 0 to n adapted events (called as EventSeq), each adapter can investigate the event and if it should indeed adapt it return the adapted event(s) for it. Other adapters which do not have anything to contribute during this adaptation simply return EventSeq.empty. The adapted events are then delivered in-order to the PersistentActorduring replay.

Note

For more advanced schema evolution techniques refer to the Persistence - Schema Evolution documentation.

Persistent FSM

PersistentFSM handles the incoming messages in an FSM like fashion. Its internal state is persisted as a sequence of changes, later referred to as domain events. Relationship between incoming messages, FSM's states and transitions, persistence of domain events is defined by a DSL.

Warning

PersistentFSM is marked as “experimental” as of its introduction in Akka 2.4.0. We will continue to improve this API based on our users’ feedback, which implies that while we try to keep incompatible changes to a minimum the binary compatibility guarantee for maintenance releases does not apply to the contents of the classes related to ``PersistentFSM`.

A Simple Example

To demonstrate the features of the PersistentFSM trait, consider an actor which represents a Web store customer. The contract of our "WebStoreCustomerFSMActor" is that it accepts the following commands:

  1. sealed trait Command
  2. case class AddItem(item: Item) extends Command
  3. case object Buy extends Command
  4. case object Leave extends Command
  5. case object GetCurrentCart extends Command

AddItem sent when the customer adds an item to a shopping cart Buy - when the customer finishes the purchaseLeave - when the customer leaves the store without purchasing anything GetCurrentCart allows to query the current state of customer's shopping cart

The customer can be in one of the following states:

  1. sealed trait UserState extends FSMState
  2. case object LookingAround extends UserState {
  3. override def identifier: String = "Looking Around"
  4. }
  5. case object Shopping extends UserState {
  6. override def identifier: String = "Shopping"
  7. }
  8. case object Inactive extends UserState {
  9. override def identifier: String = "Inactive"
  10. }
  11. case object Paid extends UserState {
  12. override def identifier: String = "Paid"
  13. }

LookingAround customer is browsing the site, but hasn't added anything to the shopping cart Shopping customer has recently added items to the shopping cart Inactive customer has items in the shopping cart, but hasn't added anything recently Paid customer has purchased the items

Note

PersistentFSM states must inherit from trait PersistentFSM.FSMState and implement the defidentifier: String method. This is required in order to simplify the serialization of FSM states. String identifiers should be unique!

Customer's actions are "recorded" as a sequence of "domain events" which are persisted. Those events are replayed on an actor's start in order to restore the latest customer's state:

  1. sealed trait DomainEvent
  2. case class ItemAdded(item: Item) extends DomainEvent
  3. case object OrderExecuted extends DomainEvent
  4. case object OrderDiscarded extends DomainEvent

Customer state data represents the items in a customer's shopping cart:

  1. case class Item(id: String, name: String, price: Float)
  2.  
  3. sealed trait ShoppingCart {
  4. def addItem(item: Item): ShoppingCart
  5. def empty(): ShoppingCart
  6. }
  7. case object EmptyShoppingCart extends ShoppingCart {
  8. def addItem(item: Item) = NonEmptyShoppingCart(item :: Nil)
  9. def empty() = this
  10. }
  11. case class NonEmptyShoppingCart(items: Seq[Item]) extends ShoppingCart {
  12. def addItem(item: Item) = NonEmptyShoppingCart(items :+ item)
  13. def empty() = EmptyShoppingCart
  14. }

Here is how everything is wired together:

  1. startWith(LookingAround, EmptyShoppingCart)
  2.  
  3. when(LookingAround) {
  4. case Event(AddItem(item), _)
  5. goto(Shopping) applying ItemAdded(item) forMax (1 seconds)
  6. case Event(GetCurrentCart, data)
  7. stay replying data
  8. }
  9.  
  10. when(Shopping) {
  11. case Event(AddItem(item), _)
  12. stay applying ItemAdded(item) forMax (1 seconds)
  13. case Event(Buy, _)
  14. goto(Paid) applying OrderExecuted andThen {
  15. case NonEmptyShoppingCart(items)
  16. reportActor ! PurchaseWasMade(items)
  17. saveStateSnapshot()
  18. case EmptyShoppingCart saveStateSnapshot()
  19. }
  20. case Event(Leave, _)
  21. stop applying OrderDiscarded andThen {
  22. case _
  23. reportActor ! ShoppingCardDiscarded
  24. saveStateSnapshot()
  25. }
  26. case Event(GetCurrentCart, data)
  27. stay replying data
  28. case Event(StateTimeout, _)
  29. goto(Inactive) forMax (2 seconds)
  30. }
  31.  
  32. when(Inactive) {
  33. case Event(AddItem(item), _)
  34. goto(Shopping) applying ItemAdded(item) forMax (1 seconds)
  35. case Event(StateTimeout, _)
  36. stop applying OrderDiscarded andThen {
  37. case _ reportActor ! ShoppingCardDiscarded
  38. }
  39. }
  40.  
  41. when(Paid) {
  42. case Event(Leave, _) stop()
  43. case Event(GetCurrentCart, data)
  44. stay replying data
  45. }

Note

State data can only be modified directly on initialization. Later it's modified only as a result of applying domain events. Override the applyEvent method to define how state data is affected by domain events, see the example below

  1. override def applyEvent(event: DomainEvent, cartBeforeEvent: ShoppingCart): ShoppingCart = {
  2. event match {
  3. case ItemAdded(item) cartBeforeEvent.addItem(item)
  4. case OrderExecuted cartBeforeEvent
  5. case OrderDiscarded cartBeforeEvent.empty()
  6. }
  7. }

andThen can be used to define actions which will be executed following event's persistence - convenient for "side effects" like sending a message or logging. Notice that actions defined in andThen block are not executed on recovery:

  1. goto(Paid) applying OrderExecuted andThen {
  2. case NonEmptyShoppingCart(items)
  3. reportActor ! PurchaseWasMade(items)
  4. }

A snapshot of state data can be persisted by calling the saveStateSnapshot() method:

  1. stop applying OrderDiscarded andThen {
  2. case _
  3. reportActor ! ShoppingCardDiscarded
  4. saveStateSnapshot()
  5. }

On recovery state data is initialized according to the latest available snapshot, then the remaining domain events are replayed, triggering the applyEvent method.

Storage plugins

Storage backends for journals and snapshot stores are pluggable in the Akka persistence extension.

A directory of persistence journal and snapshot store plugins is available at the Akka Community Projects page, seeCommunity plugins

Plugins can be selected either by "default" for all persistent actors and views, or "individually", when a persistent actor or view defines its own set of plugins.

When a persistent actor or view does NOT override the journalPluginId and snapshotPluginId methods, the persistence extension will use the "default" journal and snapshot-store plugins configured in reference.conf:

  1. akka.persistence.journal.plugin = ""
  2. akka.persistence.snapshot-store.plugin = ""

However, these entries are provided as empty "", and require explicit user configuration via override in the userapplication.conf. For an example of a journal plugin which writes messages to LevelDB see Local LevelDB journal. For an example of a snapshot store plugin which writes snapshots as individual files to the local filesystem see Local snapshot store.

Applications can provide their own plugins by implementing a plugin API and activating them by configuration. Plugin development requires the following imports:

  1. import akka.persistence._
  2. import akka.persistence.journal._
  3. import akka.persistence.snapshot._

Eager initialization of persistence plugin

By default, persistence plugins are started on-demand, as they are used. In some case, however, it might be beneficial to start a certain plugin eagerly. In order to do that, you should first add the akka.persistence.Persistence under theakka.extensions key. Then, specify the IDs of plugins you wish to start automatically underakka.persistence.journal.auto-start-journals and akka.persistence.snapshot-store.auto-start-snapshot-stores.

Journal plugin API

A journal plugin extends AsyncWriteJournal.

AsyncWriteJournal is an actor and the methods to be implemented are:

  1. /**
  2. * Plugin API: asynchronously writes a batch (`Seq`) of persistent messages to the
  3. * journal.
  4. *
  5. * The batch is only for performance reasons, i.e. all messages don't have to be written
  6. * atomically. Higher throughput can typically be achieved by using batch inserts of many
  7. * records compared to inserting records one-by-one, but this aspect depends on the
  8. * underlying data store and a journal implementation can implement it as efficient as
  9. * possible. Journals should aim to persist events in-order for a given `persistenceId`
  10. * as otherwise in case of a failure, the persistent state may be end up being inconsistent.
  11. *
  12. * Each `AtomicWrite` message contains the single `PersistentRepr` that corresponds to
  13. * the event that was passed to the `persist` method of the `PersistentActor`, or it
  14. * contains several `PersistentRepr` that corresponds to the events that were passed
  15. * to the `persistAll` method of the `PersistentActor`. All `PersistentRepr` of the
  16. * `AtomicWrite` must be written to the data store atomically, i.e. all or none must
  17. * be stored. If the journal (data store) cannot support atomic writes of multiple
  18. * events it should reject such writes with a `Try` `Failure` with an
  19. * `UnsupportedOperationException` describing the issue. This limitation should
  20. * also be documented by the journal plugin.
  21. *
  22. * If there are failures when storing any of the messages in the batch the returned
  23. * `Future` must be completed with failure. The `Future` must only be completed with
  24. * success when all messages in the batch have been confirmed to be stored successfully,
  25. * i.e. they will be readable, and visible, in a subsequent replay. If there is
  26. * uncertainty about if the messages were stored or not the `Future` must be completed
  27. * with failure.
  28. *
  29. * Data store connection problems must be signaled by completing the `Future` with
  30. * failure.
  31. *
  32. * The journal can also signal that it rejects individual messages (`AtomicWrite`) by
  33. * the returned `immutable.Seq[Try[Unit]]`. It is possible but not mandatory to reduce
  34. * number of allocations by returning `Future.successful(Nil)` for the happy path,
  35. * i.e. when no messages are rejected. Otherwise the returned `Seq` must have as many elements
  36. * as the input `messages` `Seq`. Each `Try` element signals if the corresponding
  37. * `AtomicWrite` is rejected or not, with an exception describing the problem. Rejecting
  38. * a message means it was not stored, i.e. it must not be included in a later replay.
  39. * Rejecting a message is typically done before attempting to store it, e.g. because of
  40. * serialization error.
  41. *
  42. * Data store connection problems must not be signaled as rejections.
  43. *
  44. * It is possible but not mandatory to reduce number of allocations by returning
  45. * `Future.successful(Nil)` for the happy path, i.e. when no messages are rejected.
  46. *
  47. * Calls to this method are serialized by the enclosing journal actor. If you spawn
  48. * work in asynchronous tasks it is alright that they complete the futures in any order,
  49. * but the actual writes for a specific persistenceId should be serialized to avoid
  50. * issues such as events of a later write are visible to consumers (query side, or replay)
  51. * before the events of an earlier write are visible.
  52. * A PersistentActor will not send a new WriteMessages request before the previous one
  53. * has been completed.
  54. *
  55. * Please note that the `sender` field of the contained PersistentRepr objects has been
  56. * nulled out (i.e. set to `ActorRef.noSender`) in order to not use space in the journal
  57. * for a sender reference that will likely be obsolete during replay.
  58. *
  59. * Please also note that requests for the highest sequence number may be made concurrently
  60. * to this call executing for the same `persistenceId`, in particular it is possible that
  61. * a restarting actor tries to recover before its outstanding writes have completed. In
  62. * the latter case it is highly desirable to defer reading the highest sequence number
  63. * until all outstanding writes have completed, otherwise the PersistentActor may reuse
  64. * sequence numbers.
  65. *
  66. * This call is protected with a circuit-breaker.
  67. */
  68. def asyncWriteMessages(messages: immutable.Seq[AtomicWrite]): Future[immutable.Seq[Try[Unit]]]
  69.  
  70. /**
  71. * Plugin API: asynchronously deletes all persistent messages up to `toSequenceNr`
  72. * (inclusive).
  73. *
  74. * This call is protected with a circuit-breaker.
  75. * Message deletion doesn't affect the highest sequence number of messages, journal must maintain the highest sequence number and never decrease it.
  76. */
  77. def asyncDeleteMessagesTo(persistenceId: String, toSequenceNr: Long): Future[Unit]
  78.  
  79. /**
  80. * Plugin API
  81. *
  82. * Allows plugin implementers to use `f pipeTo self` and
  83. * handle additional messages for implementing advanced features
  84. *
  85. */
  86. def receivePluginInternal: Actor.Receive = Actor.emptyBehavior

If the storage backend API only supports synchronous, blocking writes, the methods should be implemented as:

  1. def asyncWriteMessages(messages: immutable.Seq[AtomicWrite]): Future[immutable.Seq[Try[Unit]]] =
  2. Future.fromTry(Try {
  3. // blocking call here
  4. ???
  5. })

A journal plugin must also implement the methods defined in AsyncRecovery for replays and sequence number recovery:

  1. /**
  2. * Plugin API: asynchronously replays persistent messages. Implementations replay
  3. * a message by calling `replayCallback`. The returned future must be completed
  4. * when all messages (matching the sequence number bounds) have been replayed.
  5. * The future must be completed with a failure if any of the persistent messages
  6. * could not be replayed.
  7. *
  8. * The `replayCallback` must also be called with messages that have been marked
  9. * as deleted. In this case a replayed message's `deleted` method must return
  10. * `true`.
  11. *
  12. * The `toSequenceNr` is the lowest of what was returned by [[#asyncReadHighestSequenceNr]]
  13. * and what the user specified as recovery [[akka.persistence.Recovery]] parameter.
  14. * This does imply that this call is always preceded by reading the highest sequence
  15. * number for the given `persistenceId`.
  16. *
  17. * This call is NOT protected with a circuit-breaker because it may take long time
  18. * to replay all events. The plugin implementation itself must protect against
  19. * an unresponsive backend store and make sure that the returned Future is
  20. * completed with success or failure within reasonable time. It is not allowed
  21. * to ignore completing the future.
  22. *
  23. * @param persistenceId persistent actor id.
  24. * @param fromSequenceNr sequence number where replay should start (inclusive).
  25. * @param toSequenceNr sequence number where replay should end (inclusive).
  26. * @param max maximum number of messages to be replayed.
  27. * @param recoveryCallback called to replay a single message. Can be called from any
  28. * thread.
  29. *
  30. * @see [[AsyncWriteJournal]]
  31. */
  32. def asyncReplayMessages(persistenceId: String, fromSequenceNr: Long, toSequenceNr: Long,
  33. max: Long)(recoveryCallback: PersistentRepr Unit): Future[Unit]
  34.  
  35. /**
  36. * Plugin API: asynchronously reads the highest stored sequence number for the
  37. * given `persistenceId`. The persistent actor will use the highest sequence
  38. * number after recovery as the starting point when persisting new events.
  39. * This sequence number is also used as `toSequenceNr` in subsequent call
  40. * to [[#asyncReplayMessages]] unless the user has specified a lower `toSequenceNr`.
  41. * Journal must maintain the highest sequence number and never decrease it.
  42. *
  43. * This call is protected with a circuit-breaker.
  44. *
  45. * Please also note that requests for the highest sequence number may be made concurrently
  46. * to writes executing for the same `persistenceId`, in particular it is possible that
  47. * a restarting actor tries to recover before its outstanding writes have completed.
  48. *
  49. * @param persistenceId persistent actor id.
  50. * @param fromSequenceNr hint where to start searching for the highest sequence
  51. * number. When a persistent actor is recovering this
  52. * `fromSequenceNr` will be the sequence number of the used
  53. * snapshot or `0L` if no snapshot is used.
  54. */
  55. def asyncReadHighestSequenceNr(persistenceId: String, fromSequenceNr: Long): Future[Long]

A journal plugin can be activated with the following minimal configuration:

  1. # Path to the journal plugin to be used
  2. akka.persistence.journal.plugin = "my-journal"
  3.  
  4. # My custom journal plugin
  5. my-journal {
  6. # Class name of the plugin.
  7. class = "docs.persistence.MyJournal"
  8. # Dispatcher for the plugin actor.
  9. plugin-dispatcher = "akka.actor.default-dispatcher"
  10. }

The specified plugin class must have a no-arg constructor. The plugin-dispatcher is the dispatcher used for the plugin actor. If not specified, it defaults to akka.persistence.dispatchers.default-plugin-dispatcher.

The journal plugin instance is an actor so the methods corresponding to requests from persistent actors are executed sequentially. It may delegate to asynchronous libraries, spawn futures, or delegate to other actors to achive parallelism.

The journal plugin class must have a constructor without parameters or a constructor with onecom.typesafe.config.Config parameter. The plugin section of the actor system's config will be passed in the config constructor parameter.

Don't run journal tasks/futures on the system default dispatcher, since that might starve other tasks.

Snapshot store plugin API

A snapshot store plugin must extend the SnapshotStore actor and implement the following methods:

  1. /**
  2. * Plugin API: asynchronously loads a snapshot.
  3. *
  4. * This call is protected with a circuit-breaker.
  5. *
  6. * @param persistenceId id of the persistent actor.
  7. * @param criteria selection criteria for loading.
  8. */
  9. def loadAsync(persistenceId: String, criteria: SnapshotSelectionCriteria): Future[Option[SelectedSnapshot]]
  10.  
  11. /**
  12. * Plugin API: asynchronously saves a snapshot.
  13. *
  14. * This call is protected with a circuit-breaker.
  15. *
  16. * @param metadata snapshot metadata.
  17. * @param snapshot snapshot.
  18. */
  19. def saveAsync(metadata: SnapshotMetadata, snapshot: Any): Future[Unit]
  20.  
  21. /**
  22. * Plugin API: deletes the snapshot identified by `metadata`.
  23. *
  24. * This call is protected with a circuit-breaker.
  25. *
  26. * @param metadata snapshot metadata.
  27. */
  28. def deleteAsync(metadata: SnapshotMetadata): Future[Unit]
  29.  
  30. /**
  31. * Plugin API: deletes all snapshots matching `criteria`.
  32. *
  33. * This call is protected with a circuit-breaker.
  34. *
  35. * @param persistenceId id of the persistent actor.
  36. * @param criteria selection criteria for deleting.
  37. */
  38. def deleteAsync(persistenceId: String, criteria: SnapshotSelectionCriteria): Future[Unit]
  39.  
  40. /**
  41. * Plugin API
  42. * Allows plugin implementers to use `f pipeTo self` and
  43. * handle additional messages for implementing advanced features
  44. */
  45. def receivePluginInternal: Actor.Receive = Actor.emptyBehavior

A snapshot store plugin can be activated with the following minimal configuration:

  1. # Path to the snapshot store plugin to be used
  2. akka.persistence.snapshot-store.plugin = "my-snapshot-store"
  3.  
  4. # My custom snapshot store plugin
  5. my-snapshot-store {
  6. # Class name of the plugin.
  7. class = "docs.persistence.MySnapshotStore"
  8. # Dispatcher for the plugin actor.
  9. plugin-dispatcher = "akka.persistence.dispatchers.default-plugin-dispatcher"
  10. }

The specified plugin class must have a no-arg constructor. The plugin-dispatcher is the dispatcher used for the plugin actor. If not specified, it defaults to akka.persistence.dispatchers.default-plugin-dispatcher.

The snapshot store instance is an actor so the methods corresponding to requests from persistent actors are executed sequentially. It may delegate to asynchronous libraries, spawn futures, or delegate to other actors to achive parallelism.

The snapshot store plugin class must have a constructor without parameters or a constructor with onecom.typesafe.config.Config parameter. The plugin section of the actor system's config will be passed in the config constructor parameter.

Don't run snapshot store tasks/futures on the system default dispatcher, since that might starve other tasks.

Plugin TCK

In order to help developers build correct and high quality storage plugins, we provide a Technology Compatibility Kit (TCK for short).

The TCK is usable from Java as well as Scala projects. For Scala you need to include the akka-persistence-tck dependency:

  1. "com.typesafe.akka" %% "akka-persistence-tck" % "2.4.9" % "test"

To include the Journal TCK tests in your test suite simply extend the provided JournalSpec:

  1. class MyJournalSpec extends JournalSpec(
  2. config = ConfigFactory.parseString(
  3. """akka.persistence.journal.plugin = "my.journal.plugin"""")) {
  4.  
  5. override def supportsRejectingNonSerializableObjects: CapabilityFlag =
  6. false // or CapabilityFlag.off
  7. }

Please note that some of the tests are optional, and by overriding the supports... methods you give the TCK the needed information about which tests to run. You can implement these methods using boolean falues or the providedCapabilityFlag.on / CapabilityFlag.off values.

We also provide a simple benchmarking class JournalPerfSpec which includes all the tests that JournalSpec has, and also performs some longer operations on the Journal while printing its performance stats. While it is NOT aimed to provide a proper benchmarking environment it can be used to get a rough feel about your journal's performance in the most typical scenarios.

In order to include the SnapshotStore TCK tests in your test suite simply extend the SnapshotStoreSpec:

  1. class MySnapshotStoreSpec extends SnapshotStoreSpec(
  2. config = ConfigFactory.parseString(
  3. """
  4. akka.persistence.snapshot-store.plugin = "my.snapshot-store.plugin"
  5. """))

In case your plugin requires some setting up (starting a mock database, removing temporary files etc.) you can override the beforeAll and afterAll methods to hook into the tests lifecycle:

  1. class MyJournalSpec extends JournalSpec(
  2. config = ConfigFactory.parseString(
  3. """
  4. akka.persistence.journal.plugin = "my.journal.plugin"
  5. """)) {
  6.  
  7. override def supportsRejectingNonSerializableObjects: CapabilityFlag =
  8. true // or CapabilityFlag.on
  9.  
  10. val storageLocations = List(
  11. new File(system.settings.config.getString("akka.persistence.journal.leveldb.dir")),
  12. new File(config.getString("akka.persistence.snapshot-store.local.dir")))
  13.  
  14. override def beforeAll() {
  15. super.beforeAll()
  16. storageLocations foreach FileUtils.deleteRecursively
  17. }
  18.  
  19. override def afterAll() {
  20. storageLocations foreach FileUtils.deleteRecursively
  21. super.afterAll()
  22. }
  23.  
  24. }

We highly recommend including these specifications in your test suite, as they cover a broad range of cases you might have otherwise forgotten to test for when writing a plugin from scratch.

Pre-packaged plugins

Local LevelDB journal

The LevelDB journal plugin config entry is akka.persistence.journal.leveldb. It writes messages to a local LevelDB instance. Enable this plugin by defining config property:

  1. # Path to the journal plugin to be used
  2. akka.persistence.journal.plugin = "akka.persistence.journal.leveldb"

LevelDB based plugins will also require the following additional dependency declaration:

  1. "org.iq80.leveldb" % "leveldb" % "0.7"
  2. "org.fusesource.leveldbjni" % "leveldbjni-all" % "1.8"

The default location of LevelDB files is a directory named journal in the current working directory. This location can be changed by configuration where the specified path can be relative or absolute:

  1. akka.persistence.journal.leveldb.dir = "target/journal"

With this plugin, each actor system runs its own private LevelDB instance.

Shared LevelDB journal

A LevelDB instance can also be shared by multiple actor systems (on the same or on different nodes). This, for example, allows persistent actors to failover to a backup node and continue using the shared journal instance from the backup node.

Warning

A shared LevelDB instance is a single point of failure and should therefore only be used for testing purposes. Highly-available, replicated journals are available as Community plugins.

Note

This plugin has been supplanted by Persistence Plugin Proxy.

A shared LevelDB instance is started by instantiating the SharedLeveldbStore actor.

  1. import akka.persistence.journal.leveldb.SharedLeveldbStore
  2.  
  3. val store = system.actorOf(Props[SharedLeveldbStore], "store")

By default, the shared instance writes journaled messages to a local directory named journal in the current working directory. The storage location can be changed by configuration:

  1. akka.persistence.journal.leveldb-shared.store.dir = "target/shared"

Actor systems that use a shared LevelDB store must activate the akka.persistence.journal.leveldb-sharedplugin.

  1. akka.persistence.journal.plugin = "akka.persistence.journal.leveldb-shared"

This plugin must be initialized by injecting the (remote) SharedLeveldbStore actor reference. Injection is done by calling the SharedLeveldbJournal.setStore method with the actor reference as argument.

  1. trait SharedStoreUsage extends Actor {
  2. override def preStart(): Unit = {
  3. context.actorSelection("akka.tcp://example@127.0.0.1:2552/user/store") ! Identify(1)
  4. }
  5.  
  6. def receive = {
  7. case ActorIdentity(1, Some(store)) =>
  8. SharedLeveldbJournal.setStore(store, context.system)
  9. }
  10. }

Internal journal commands (sent by persistent actors) are buffered until injection completes. Injection is idempotent i.e. only the first injection is used.

Local snapshot store

The local snapshot store plugin config entry is akka.persistence.snapshot-store.local. It writes snapshot files to the local filesystem. Enable this plugin by defining config property:

  1. # Path to the snapshot store plugin to be used
  2. akka.persistence.snapshot-store.plugin = "akka.persistence.snapshot-store.local"

The default storage location is a directory named snapshots in the current working directory. This can be changed by configuration where the specified path can be relative or absolute:

  1. akka.persistence.snapshot-store.local.dir = "target/snapshots"

Note that it is not mandatory to specify a snapshot store plugin. If you don't use snapshots you don't have to configure it.

Persistence Plugin Proxy

A persistence plugin proxy allows sharing of journals and snapshot stores across multiple actor systems (on the same or on different nodes). This, for example, allows persistent actors to failover to a backup node and continue using the shared journal instance from the backup node. The proxy works by forwarding all the journal/snapshot store messages to a single, shared, persistence plugin instance, and therefor supports any use case supported by the proxied plugin.

Warning

A shared journal/snapshot store is a single point of failure and should therefore only be used for testing purposes. Highly-available, replicated persistence plugins are available as Community plugins.

The journal and snapshot store proxies are controlled via the akka.persistence.journal.proxy andakka.persistence.snapshot-store.proxy configuration entries, respectively. Set the target-journal-pluginor target-snapshot-store-plugin keys to the underlying plugin you wish to use (for example:akka.persistence.journal.leveldb). The start-target-journal and start-target-snapshot-store keys should be set to on in exactly one actor system - this is the system that will instantiate the shared persistence plugin. Next, the proxy needs to be told how to find the shared plugin. This can be done by setting the target-journal-address and target-snapshot-store-address configuration keys, or programmatically by calling thePersistencePluginProxy.setTargetLocation method.

Note

Akka starts extensions lazily when they are required, and this includes the proxy. This means that in order for the proxy to work, the persistence plugin on the target node must be instantiated. This can be done by instantiating the PersistencePluginProxyExtension extension, or by calling the PersistencePluginProxy.startmethod.

Note

The proxied persistence plugin can (and should) be configured using its original configuration keys.

Custom serialization

Serialization of snapshots and payloads of Persistent messages is configurable with Akka's Serializationinfrastructure. For example, if an application wants to serialize

it must add

  1. akka.actor {
  2. serializers {
  3. my-payload = "docs.persistence.MyPayloadSerializer"
  4. my-snapshot = "docs.persistence.MySnapshotSerializer"
  5. }
  6. serialization-bindings {
  7. "docs.persistence.MyPayload" = my-payload
  8. "docs.persistence.MySnapshot" = my-snapshot
  9. }
  10. }

to the application configuration. If not specified, a default serializer is used.

For more advanced schema evolution techniques refer to the Persistence - Schema Evolution documentation.

Testing

When running tests with LevelDB default settings in sbt, make sure to set fork := true in your sbt project. Otherwise, you'll see an UnsatisfiedLinkError. Alternatively, you can switch to a LevelDB Java port by setting

  1. akka.persistence.journal.leveldb.native = off

or

  1. akka.persistence.journal.leveldb-shared.store.native = off

in your Akka configuration. The LevelDB Java port is for testing purposes only.

Warning

It is not possible to test persistence provided classes (i.e. PersistentActor and AtLeastOnceDelivery) usingTestActorRef due to its synchronous nature. These traits need to be able to perform asynchronous tasks in the background in order to handle internal persistence related events.

When testing Persistence based projects always rely on asynchronous messaging using the TestKit.

Configuration

There are several configuration properties for the persistence module, please refer to the reference configuration.

Multiple persistence plugin configurations

By default, a persistent actor or view will use the "default" journal and snapshot store plugins configured in the following sections of the reference.conf configuration resource:

  1. # Absolute path to the default journal plugin configuration entry.
  2. akka.persistence.journal.plugin = "akka.persistence.journal.inmem"
  3. # Absolute path to the default snapshot store plugin configuration entry.
  4. akka.persistence.snapshot-store.plugin = "akka.persistence.snapshot-store.local"

Note that in this case the actor or view overrides only the persistenceId method:

  1. trait ActorWithDefaultPlugins extends PersistentActor {
  2. override def persistenceId = "123"
  3. }

When the persistent actor or view overrides the journalPluginId and snapshotPluginId methods, the actor or view will be serviced by these specific persistence plugins instead of the defaults:

  1. trait ActorWithOverridePlugins extends PersistentActor {
  2. override def persistenceId = "123"
  3. // Absolute path to the journal plugin configuration entry in the `reference.conf`.
  4. override def journalPluginId = "akka.persistence.chronicle.journal"
  5. // Absolute path to the snapshot store plugin configuration entry in the `reference.conf`.
  6. override def snapshotPluginId = "akka.persistence.chronicle.snapshot-store"
  7. }

Note that journalPluginId and snapshotPluginId must refer to properly configured reference.conf plugin entries with a standard class property as well as settings which are specific for those plugins, i.e.:

  1. # Configuration entry for the custom journal plugin, see `journalPluginId`.
  2. akka.persistence.chronicle.journal {
  3. # Standard persistence extension property: provider FQCN.
  4. class = "akka.persistence.chronicle.ChronicleSyncJournal"
  5. # Custom setting specific for the journal `ChronicleSyncJournal`.
  6. folder = $${user.dir}/store/journal
  7. }
  8. # Configuration entry for the custom snapshot store plugin, see `snapshotPluginId`.
  9. akka.persistence.chronicle.snapshot-store {
  10. # Standard persistence extension property: provider FQCN.
  11. class = "akka.persistence.chronicle.ChronicleSnapshotStore"
  12. # Custom setting specific for the snapshot store `ChronicleSnapshotStore`.
  13. folder = $${user.dir}/store/snapshot
  14. }

Persistence - Schema Evolution

When working on long running projects using Persistence, or any kind of Event Sourcing architectures, schema evolution becomes one of the more important technical aspects of developing your application. The requirements as well as our own understanding of the business domain may (and will) change in time.

In fact, if a project matures to the point where you need to evolve its schema to adapt to changing business requirements you can view this as first signs of its success – if you wouldn't need to adapt anything over an apps lifecycle that could mean that no-one is really using it actively.

In this chapter we will investigate various schema evolution strategies and techniques from which you can pick and choose the ones that match your domain and challenge at hand.

Note

This page proposes a number of possible solutions to the schema evolution problem and explains how some of the utilities Akka provides can be used to achieve this, it is by no means a complete (closed) set of solutions.

Sometimes, based on the capabilities of your serialization formats, you may be able to evolve your schema in different ways than outlined in the sections below. If you discover useful patterns or techniques for schema evolution feel free to submit Pull Requests to this page to extend it.

Schema evolution in event-sourced systems

In recent years we have observed a tremendous move towards immutable append-only datastores, with event-sourcing being the prime technique successfully being used in these settings. For an excellent overview why and how immutable data makes scalability and systems design much simpler you may want to read Pat Helland's excellent Immutability Changes Everything whitepaper.

Since with Event Sourcing the events are immutable and usually never deleted – the way schema evolution is handled differs from how one would go about it in a mutable database setting (e.g. in typical CRUD database applications).

The system needs to be able to continue to work in the presence of "old" events which were stored under the "old" schema. We also want to limit complexity in the business logic layer, exposing a consistent view over all of the events of a given type to PersistentActor s and persistence queries. This allows the business logic layer to focus on solving business problems instead of having to explicitly deal with different schemas.

In summary, schema evolution in event sourced systems exposes the following characteristics:
  • Allow the system to continue operating without large scale migrations to be applied,
  • Allow the system to read "old" events from the underlying storage, however present them in a "new" view to the application logic,
  • Transparently promote events to the latest versions during recovery (or queries) such that the business logic need not consider multiple versions of events

Types of schema evolution

Before we explain the various techniques that can be used to safely evolve the schema of your persistent events over time, we first need to define what the actual problem is, and what the typical styles of changes are.

Since events are never deleted, we need to have a way to be able to replay (read) old events, in such way that does not force the PersistentActor to be aware of all possible versions of an event that it may have persisted in the past. Instead, we want the Actors to work on some form of "latest" version of the event and provide some means of either converting old "versions" of stored events into this "latest" event type, or constantly evolve the event definition - in a backwards compatible way - such that the new deserialization code can still read old events.

The most common schema changes you will likely are:

The following sections will explain some patterns which can be used to safely evolve your schema when facing those changes.

Picking the right serialization format

Picking the serialization format is a very important decision you will have to make while building your application. It affects which kind of evolutions are simple (or hard) to do, how much work is required to add a new datatype, and, last but not least, serialization performance.

If you find yourself realising you have picked "the wrong" serialization format, it is always possible to change the format used for storing new events, however you would have to keep the old deserialization code in order to be able to replay events that were persisted using the old serialization scheme. It is possible to "rebuild" an event-log from one serialization format to another one, however it may be a more involved process if you need to perform this on a live system.

Binary serialization formats that we have seen work well for long-lived applications include the very flexible IDL based:Google Protobuf, Apache Thrift or Apache Avro. Avro schema evolution is more "entire schema" based, instead of single fields focused like in protobuf or thrift, and usually requires using some kind of schema registry.

Users who want their data to be human-readable directly in the write-side datastore may opt to use plain-old JSON as the storage format, though that comes at a cost of lacking support for schema evolution and relatively large marshalling latency.

There are plenty excellent blog posts explaining the various trade-offs between popular serialization formats, one post we would like to highlight is the very well illustrated Schema evolution in Avro, Protocol Buffers and Thrift by Martin Kleppmann.

Provided default serializers

Akka Persistence provides Google Protocol Buffers based serializers (using Akka Serialization) for it's own message types such as PersistentRepr, AtomicWrite and snapshots. Journal plugin implementations may choose to use those provided serializers, or pick a serializer which suits the underlying database better.

Note

Serialization is NOT handled automatically by Akka Persistence itself. Instead, it only provides the above described serializers, and in case a AsyncWriteJournal plugin implementation chooses to use them directly, the above serialization scheme will be used.

Please refer to your write journal's documentation to learn more about how it handles serialization!

For example, some journals may choose to not use Akka Serialization at all and instead store the data in a format that is more "native" for the underlying datastore, e.g. using JSON or some other kind of format that the target datastore understands directly.

The below figure explains how the default serialization scheme works, and how it fits together with serializing the user provided message itself, which we will from here on refer to as the payload (highlighted in yellow):

../_images/persistent-message-envelope1.png

Akka Persistence provided serializers wrap the user payload in an envelope containing all persistence-relevant information. If the Journal uses provided Protobuf serializers for the wrapper types (e.g. PersistentRepr), then the payload will be serialized using the user configured serializer, and if none is provided explicitly, Java serialization will be used for it.

The blue colored regions of the PersistentMessage indicate what is serialized using the generated protocol buffers serializers, and the yellow payload indicates the user provided event (by calling persist(payload)(...)). As you can see, the PersistentMessage acts as an envelope around the payload, adding various fields related to the origin of the event (persistenceId, sequenceNr and more).

More advanced techniques (e.g. Remove event class and ignore events) will dive into using the manifests for increasing the flexibility of the persisted vs. exposed types even more. However for now we will focus on the simpler evolution techniques, concerning simply configuring the payload serializers.

By default the payload will be serialized using Java Serialization. This is fine for testing and initial phases of your development (while you're still figuring out things and the data will not need to stay persisted forever). However, once you move to production you should really pick a different serializer for your payloads.

Warning

Do not rely on Java serialization (which will be picked by Akka by default if you don't specify any serializers) forserious application development! It does not lean itself well to evolving schemas over long periods of time, and its performance is also not very high (it never was designed for high-throughput scenarios).

Configuring payload serializers

This section aims to highlight the complete basics on how to define custom serializers using Akka Serialization. Many journal plugin implementations use Akka Serialization, thus it is tremendously important to understand how to configure it to work with your event classes.

Note

Read the Akka Serialization docs to learn more about defining custom serializers, to improve performance and maintainability of your system. Do not depend on Java serialization for production deployments.

The below snippet explains in the minimal amount of lines how a custom serializer can be registered. For more in-depth explanations on how serialization picks the serializer to use etc, please refer to its documentation.

First we start by defining our domain model class, here representing a person:

  1. final case class Person(name: String, surname: String)

Next we implement a serializer (or extend an existing one to be able to handle the new Person class):

  1. /**
  2. * Simplest possible serializer, uses a string representation of the Person class.
  3. *
  4. * Usually a serializer like this would use a library like:
  5. * protobuf, kryo, avro, cap'n proto, flatbuffers, SBE or some other dedicated serializer backend
  6. * to perform the actual to/from bytes marshalling.
  7. */
  8. class SimplestPossiblePersonSerializer extends SerializerWithStringManifest {
  9. val Utf8 = Charset.forName("UTF-8")
  10.  
  11. val PersonManifest = classOf[Person].getName
  12.  
  13. // unique identifier of the serializer
  14. def identifier = 1234567
  15.  
  16. // extract manifest to be stored together with serialized object
  17. override def manifest(o: AnyRef): String = o.getClass.getName
  18.  
  19. // serialize the object
  20. override def toBinary(obj: AnyRef): Array[Byte] = obj match {
  21. case p: Person => s"""${p.name}|${p.surname}""".getBytes(Utf8)
  22. case _ => throw new IllegalArgumentException(
  23. s"Unable to serialize to bytes, clazz was: ${obj.getClass}!")
  24. }
  25.  
  26. // deserialize the object, using the manifest to indicate which logic to apply
  27. override def fromBinary(bytes: Array[Byte], manifest: String): AnyRef =
  28. manifest match {
  29. case PersonManifest =>
  30. val nameAndSurname = new String(bytes, Utf8)
  31. val Array(name, surname) = nameAndSurname.split("[|]")
  32. Person(name, surname)
  33. case _ => throw new IllegalArgumentException(
  34. s"Unable to deserialize from bytes, manifest was: $manifest! Bytes length: " +
  35. bytes.length)
  36. }
  37.  
  38. }

And finally we register the serializer and bind it to handle the docs.persistence.Person class:

  1. # application.conf
  2. akka {
  3. actor {
  4. serializers {
  5. person = "docs.persistence.SimplestPossiblePersonSerializer"
  6. }
  7.  
  8. serialization-bindings {
  9. "docs.persistence.Person" = person
  10. }
  11. }
  12. }

Deserialization will be performed by the same serializer which serialized the message initially because of theidentifier being stored together with the message.

Please refer to the Akka Serialization documentation for more advanced use of serializers, especially the Serializer with String Manifest section since it is very useful for Persistence based applications dealing with schema evolutions, as we will see in some of the examples below.

Schema evolution in action

In this section we will discuss various schema evolution techniques using concrete examples and explaining some of the various options one might go about handling the described situation. The list below is by no means a complete guide, so feel free to adapt these techniques depending on your serializer's capabilities and/or other domain specific limitations.

Add fields

Situation: You need to add a field to an existing message type. For example, a SeatReservation(letter:String,row:Int) now needs to have an associated code which indicates if it is a window or aisle seat.

Solution: Adding fields is the most common change you'll need to apply to your messages so make sure the serialization format you picked for your payloads can handle it apropriately, i.e. such changes should be binary compatible. This is easily achieved using the right serializer toolkit – we recommend something like Google Protocol Buffers or Apache Thrift however other tools may fit your needs just as well – picking a serializer backend is something you should research before picking one to run with. In the following examples we will be using protobuf, mostly because we are familiar with it, it does its job well and Akka is using it internally as well.

While being able to read messages with missing fields is half of the solution, you also need to deal with the missing values somehow. This is usually modeled as some kind of default value, or by representing the field as an Option[T]See below for an example how reading an optional field from a serialized protocol buffers message might look like.

  1. sealed abstract class SeatType { def code: String }
  2. object SeatType {
  3. def fromString(s: String) = s match {
  4. case Window.code => Window
  5. case Aisle.code => Aisle
  6. case Other.code => Other
  7. case _ => Unknown
  8. }
  9. case object Window extends SeatType { override val code = "W" }
  10. case object Aisle extends SeatType { override val code = "A" }
  11. case object Other extends SeatType { override val code = "O" }
  12. case object Unknown extends SeatType { override val code = "" }
  13.  
  14. }
  15.  
  16. case class SeatReserved(letter: String, row: Int, seatType: SeatType)

Next we prepare an protocol definition using the protobuf Interface Description Language, which we'll use to generate the serializer code to be used on the Akka Serialization layer (notice that the schema aproach allows us to easily rename fields, as long as the numeric identifiers of the fields do not change):

  1. // FlightAppModels.proto
  2. option java_package = "docs.persistence.proto";
  3. option optimize_for = SPEED;
  4.  
  5. message SeatReserved {
  6. required string letter = 1;
  7. required uint32 row = 2;
  8. optional string seatType = 3; // the new field
  9. }

The serializer implementation uses the protobuf generated classes to marshall the payloads. Optional fields can be handled explicitly or missing values by calling the has... methods on the protobuf object, which we do forseatType in order to use a Unknown type in case the event was stored before we had introduced the field to this event type:

  1. /**
  2. * Example serializer impl which uses protocol buffers generated classes (proto.*)
  3. * to perform the to/from binary marshalling.
  4. */
  5. class AddedFieldsSerializerWithProtobuf extends SerializerWithStringManifest {
  6. override def identifier = 67876
  7.  
  8. final val SeatReservedManifest = classOf[SeatReserved].getName
  9.  
  10. override def manifest(o: AnyRef): String = o.getClass.getName
  11.  
  12. override def fromBinary(bytes: Array[Byte], manifest: String): AnyRef =
  13. manifest match {
  14. case SeatReservedManifest =>
  15. // use generated protobuf serializer
  16. seatReserved(FlightAppModels.SeatReserved.parseFrom(bytes))
  17. case _ =>
  18. throw new IllegalArgumentException("Unable to handle manifest: " + manifest)
  19. }
  20.  
  21. override def toBinary(o: AnyRef): Array[Byte] = o match {
  22. case s: SeatReserved =>
  23. FlightAppModels.SeatReserved.newBuilder
  24. .setRow(s.row)
  25. .setLetter(s.letter)
  26. .setSeatType(s.seatType.code)
  27. .build().toByteArray
  28. }
  29.  
  30. // -- fromBinary helpers --
  31.  
  32. private def seatReserved(p: FlightAppModels.SeatReserved): SeatReserved =
  33. SeatReserved(p.getLetter, p.getRow, seatType(p))
  34.  
  35. // handle missing field by assigning "Unknown" value
  36. private def seatType(p: FlightAppModels.SeatReserved): SeatType =
  37. if (p.hasSeatType) SeatType.fromString(p.getSeatType) else SeatType.Unknown
  38.  
  39. }

Rename fields

Situation: When first designing the system the SeatReverved event featured an code field. After some time you discover that what was originally called code actually means seatNr, thus the model should be changed to reflect this concept more accurately.

Solution 1 - using IDL based serializers: First, we will discuss the most efficient way of dealing with such kinds of schema changes – IDL based serializers.

IDL stands for Interface Description Language, and means that the schema of the messages that will be stored is based on this description. Most IDL based serializers also generate the serializer / deserializer code so that using them is not too hard. Examples of such serializers are protobuf or thrift.

Using these libraries rename operations are "free", because the field name is never actually stored in the binary representation of the message. This is one of the advantages of schema based serializers, even though that they add the overhead of having to maintain the schema. When using serializers like this, no additional code change (except renaming the field and method used during serialization) is needed to perform such evolution:

../_images/persistence-serializer-rename1.png

This is how such a rename would look in protobuf:

  1. // protobuf message definition, BEFORE:
  2. message SeatReserved {
  3. required string code = 1;
  4. }
  5.  
  6. // protobuf message definition, AFTER:
  7. message SeatReserved {
  8. required string seatNr = 1; // field renamed, id remains the same
  9. }

It is important to learn about the strengths and limitations of your serializers, in order to be able to move swiftly and refactor your models fearlessly as you go on with the project.

Note

Learn in-depth about the serialization engine you're using as it will impact how you can aproach schema evolution.

Some operations are "free" in certain serialization formats (more often than not: removing/adding optional fields, sometimes renaming fields etc.), while some other operations are strictly not possible.

Solution 2 - by manually handling the event versions: Another solution, in case your serialization format does not support renames as easily as the above mentioned formats, is versioning your schema. For example, you could have made your events carry an additional field called _version which was set to 1 (because it was the initial schema), and once you change the schema you bump this number to 2, and write an adapter which can perform the rename.

This approach is popular when your serialization format is something like JSON, where renames can not be performed automatically by the serializer. You can do these kinds of "promotions" either manually (as shown in the example below) or using a library like Stamina which helps to create those V1->V2->V3->...->Vn promotion chains without much boilerplate.

../_images/persistence-manual-rename1.png

The following snippet showcases how one could apply renames if working with plain JSON (usingspray.json.JsObject):

  1. class JsonRenamedFieldAdapter extends EventAdapter {
  2. val marshaller = new ExampleJsonMarshaller
  3.  
  4. val V1 = "v1"
  5. val V2 = "v2"
  6.  
  7. // this could be done independently for each event type
  8. override def manifest(event: Any): String = V2
  9.  
  10. override def toJournal(event: Any): JsObject =
  11. marshaller.toJson(event)
  12.  
  13. override def fromJournal(event: Any, manifest: String): EventSeq = event match {
  14. case json: JsObject => EventSeq(marshaller.fromJson(manifest match {
  15. case V1 => rename(json, "code", "seatNr")
  16. case V2 => json // pass-through
  17. case unknown => throw new IllegalArgumentException(s"Unknown manifest: $unknown")
  18. }))
  19. case _ =>
  20. val c = event.getClass
  21. throw new IllegalArgumentException("Can only work with JSON, was: %s".format(c))
  22. }
  23.  
  24. def rename(json: JsObject, from: String, to: String): JsObject = {
  25. val value = json.fields(from)
  26. val withoutOld = json.fields - from
  27. JsObject(withoutOld + (to -> value))
  28. }
  29.  
  30. }

As you can see, manually handling renames induces some boilerplate onto the EventAdapter, however much of it you will find is common infrastructure code that can be either provided by an external library (for promotion management) or put together in a simple helper trait.

Note

The technique of versioning events and then promoting them to the latest version using JSON transformations can of course be applied to more than just field renames – it also applies to adding fields and all kinds of changes in the message format.

Remove event class and ignore events

Situation: While investigating app performance you notice that insane amounts of CustomerBlinked events are being stored for every customer each time he/she blinks. Upon investigation you decide that the event does not add any value and should be deleted. You still have to be able to replay from a journal which contains those old CustomerBlinked events though.

Naive solution - drop events in EventAdapter:

The problem of removing an event type from the domain model is not as much its removal, as the implications for the recovery mechanisms that this entails. For example, a naive way of filtering out certain kinds of events from being delivered to a recovering PersistentActor is pretty simple, as one can simply filter them out in an EventAdapter:

../_images/persistence-drop-event1.png

The EventAdapter can drop old events (O) by emitting an empty EventSeq. Other events can simply be passed through (E).

This however does not address the underlying cost of having to deserialize all the events during recovery, even those which will be filtered out by the adapter. In the next section we will improve the above explained mechanism to avoid deserializing events which would be filtered out by the adapter anyway, thus allowing to save precious time during a recovery containing lots of such events (without actually having to delete them).

Improved solution - deserialize into tombstone:

In the just described technique we have saved the PersistentActor from receiving un-wanted events by filtering them out in the EventAdapter, however the event itself still was deserialized and loaded into memory. This has two notabledownsides:

  • first, that the deserialization was actually performed, so we spent some of out time budget on the deserialization, even though the event does not contribute anything to the persistent actors state.
  • second, that we are unable to remove the event class from the system – since the serializer still needs to create the actuall instance of it, as it does not know it will not be used.

The solution to these problems is to use a serializer that is aware of that event being no longer needed, and can notice this before starting to deserialize the object.

This aproach allows us to remove the original class from our classpath, which makes for less "old" classes lying around in the project. This can for example be implemented by using an SerializerWithStringManifest (documented in depth in Serializer with String Manifest). By looking at the string manifest, the serializer can notice that the type is no longer needed, and skip the deserialization all-together:

../_images/persistence-drop-event-serializer1.png

The serializer is aware of the old event types that need to be skipped (O), and can skip deserializing them alltogether by simply returning a "tombstone" (T), which the EventAdapter converts into an empty EventSeq. Other events (E) can simply be passed through.

The serializer detects that the string manifest points to a removed event type and skips attempting to deserialize it:

  1. case object EventDeserializationSkipped
  2.  
  3. class RemovedEventsAwareSerializer extends SerializerWithStringManifest {
  4. val utf8 = Charset.forName("UTF-8")
  5. override def identifier: Int = 8337
  6.  
  7. val SkipEventManifestsEvents = Set(
  8. "docs.persistence.CustomerBlinked" // ...
  9. )
  10.  
  11. override def manifest(o: AnyRef): String = o.getClass.getName
  12.  
  13. override def toBinary(o: AnyRef): Array[Byte] = o match {
  14. case _ => o.toString.getBytes(utf8) // example serialization
  15. }
  16.  
  17. override def fromBinary(bytes: Array[Byte], manifest: String): AnyRef =
  18. manifest match {
  19. case m if SkipEventManifestsEvents.contains(m) =>
  20. EventDeserializationSkipped
  21.  
  22. case other => new String(bytes, utf8)
  23. }
  24. }

The EventAdapter we implemented is aware of EventDeserializationSkipped events (our "Tombstones"), and emits and empty EventSeq whenever such object is encoutered:

  1. class SkippedEventsAwareAdapter extends EventAdapter {
  2. override def manifest(event: Any) = ""
  3. override def toJournal(event: Any) = event
  4.  
  5. override def fromJournal(event: Any, manifest: String) = event match {
  6. case EventDeserializationSkipped => EventSeq.empty
  7. case _ => EventSeq(event)
  8. }
  9. }

Detach domain model from data model

Situation: You want to separate the application model (often called the "domain model") completely from the models used to persist the corresponding events (the "data model"). For example because the data representation may change independently of the domain model.

Another situation where this technique may be useful is when your serialization tool of choice requires generated classes to be used for serialization and deserialization of objects, like for example Google Protocol Buffers do, yet you do not want to leak this implementation detail into the domain model itself, which you'd like to model as plain Scala case classes.

Solution: In order to detach the domain model, which is often represented using pure scala (case) classes, from the data model classes which very often may be less user-friendly yet highly optimised for throughput and schema evolution (like the classes generated by protobuf for example), it is possible to use a simple EventAdapter which maps between these types in a 1:1 style as illustrated below:

../_images/persistence-detach-models1.png

Domain events (A) are adapted to the data model events (D) by the EventAdapter. The data model can be a format natively understood by the journal, such that it can store it more efficiently or include additional data for the event (e.g. tags), for ease of later querying.

We will use the following domain and data models to showcase how the separation can be implemented by the adapter:

  1. /** Domain model - highly optimised for domain language and maybe "fluent" usage */
  2. object DomainModel {
  3. final case class Customer(name: String)
  4. final case class Seat(code: String) {
  5. def bookFor(customer: Customer): SeatBooked = SeatBooked(code, customer)
  6. }
  7.  
  8. final case class SeatBooked(code: String, customer: Customer)
  9. }
  10.  
  11. /** Data model - highly optimised for schema evolution and persistence */
  12. object DataModel {
  13. final case class SeatBooked(code: String, customerName: String)
  14. }

The EventAdapter takes care of converting from one model to the other one (in both directions), alowing the models to be completely detached from each other, such that they can be optimised independently as long as the mapping logic is able to convert between them:

  1. class DetachedModelsAdapter extends EventAdapter {
  2. override def manifest(event: Any): String = ""
  3.  
  4. override def toJournal(event: Any): Any = event match {
  5. case DomainModel.SeatBooked(code, customer) =>
  6. DataModel.SeatBooked(code, customer.name)
  7. }
  8. override def fromJournal(event: Any, manifest: String): EventSeq = event match {
  9. case DataModel.SeatBooked(code, customerName) =>
  10. EventSeq(DomainModel.SeatBooked(code, DomainModel.Customer(customerName)))
  11. }
  12. }

The same technique could also be used directly in the Serializer if the end result of marshalling is bytes. Then the serializer can simply convert the bytes do the domain object by using the generated protobuf builders.

Store events as human-readable data model

Situation: You want to keep your persisted events in a human-readable format, for example JSON.

Solution: This is a special case of the Detach domain model from data model pattern, and thus requires some co-operation from the Journal implementation to achieve this.

An example of a Journal which may implement this pattern is MongoDB, however other databases such as PostgreSQL and Cassandra could also do it because of their built-in JSON capabilities.

In this aproach, the EventAdapter is used as the marshalling layer: it serializes the events to/from JSON. The journal plugin notices that the incoming event type is JSON (for example by performing a match on the incoming event) and stores the incoming object directly.

  1. class JsonDataModelAdapter extends EventAdapter {
  2. override def manifest(event: Any): String = ""
  3.  
  4. val marshaller = new ExampleJsonMarshaller
  5.  
  6. override def toJournal(event: Any): JsObject =
  7. marshaller.toJson(event)
  8.  
  9. override def fromJournal(event: Any, manifest: String): EventSeq = event match {
  10. case json: JsObject =>
  11. EventSeq(marshaller.fromJson(json))
  12. case _ =>
  13. throw new IllegalArgumentException(
  14. "Unable to fromJournal a non-JSON object! Was: " + event.getClass)
  15. }
  16. }

Note

This technique only applies if the Akka Persistence plugin you are using provides this capability. Check the documentation of your favourite plugin to see if it supports this style of persistence.

If it doesn't, you may want to skim the list of existing journal plugins, just in case some other plugin for your favourite datastore does provide this capability.

Alternative solution:

In fact, an AsyncWriteJournal implementation could natively decide to not use binary serialization at all, and alwaysserialize the incoming messages as JSON - in which case the toJournal implementation of the EventAdapter would be an identity function, and the fromJournal would need to de-serialize messages from JSON.

Note

If in need of human-readable events on the write-side of your application reconsider whether preparing materialized views using Persistence Query would not be an efficient way to go about this, without compromising the write-side's throughput characteristics.

If indeed you want to use a human-readable representation on the write-side, pick a Persistence plugin that provides that functionality, or – implement one yourself.

Split large event into fine-grained events

Situation: While refactoring your domain events, you find that one of the events has become too large (coarse-grained) and needs to be split up into multiple fine-grained events.

Solution: Let us consider a situation where an event represents "user details changed". After some time we discover that this event is too coarse, and needs to be split into "user name changed" and "user address changed", because somehow users keep changing their usernames a lot and we'd like to keep this as a separate event.

The write side change is very simple, we simply persist UserNameChanged or UserAddressChanged depending on what the user actually intended to change (instead of the composite UserDetailsChanged that we had in version 1 of our model).

../_images/persistence-event-adapter-1-n1.png

The EventAdapter splits the incoming event into smaller more fine grained events during recovery.

During recovery however, we now need to convert the old V1 model into the V2 representation of the change. Depending if the old event contains a name change, we either emit the UserNameChanged or we don't, and the address change is handled similarily:

  1. trait V1
  2. trait V2
  3.  
  4. // V1 event:
  5. final case class UserDetailsChanged(name: String, address: String) extends V1
  6.  
  7. // corresponding V2 events:
  8. final case class UserNameChanged(name: String) extends V2
  9. final case class UserAddressChanged(address: String) extends V2
  10.  
  11. // event splitting adapter:
  12. class UserEventsAdapter extends EventAdapter {
  13. override def manifest(event: Any): String = ""
  14.  
  15. override def fromJournal(event: Any, manifest: String): EventSeq = event match {
  16. case UserDetailsChanged(null, address) => EventSeq(UserAddressChanged(address))
  17. case UserDetailsChanged(name, null) => EventSeq(UserNameChanged(name))
  18. case UserDetailsChanged(name, address) =>
  19. EventSeq(
  20. UserNameChanged(name),
  21. UserAddressChanged(address))
  22. case event: V2 => EventSeq(event)
  23. }
  24.  
  25. override def toJournal(event: Any): Any = event
  26. }

By returning an EventSeq from the event adapter, the recovered event can be converted to multiple events before being delivered to the persistent actor.

Persistence Query

Akka persistence query complements Persistence by providing a universal asynchronous stream based query interface that various journal plugins can implement in order to expose their query capabilities.

The most typical use case of persistence query is implementing the so-called query side (also known as "read side") in the popular CQRS architecture pattern - in which the writing side of the application (e.g. implemented using akka persistence) is completely separated from the "query side". Akka Persistence Query itself is not directly the query side of an application, however it can help to migrate data from the write side to the query side database. In very simple scenarios Persistence Query may be powerful enough to fulfill the query needs of your app, however we highly recommend (in the spirit of CQRS) of splitting up the write/read sides into separate datastores as the need arises.

Warning

This module is marked as “experimental” as of its introduction in Akka 2.4.0. We will continue to improve this API based on our users’ feedback, which implies that while we try to keep incompatible changes to a minimum the binary compatibility guarantee for maintenance releases does not apply to the contents of theakka.persistence.query package.

Dependencies

Akka persistence query is a separate jar file. Make sure that you have the following dependency in your project:

  1. "com.typesafe.akka" %% "akka-persistence-query-experimental" % "2.4.9"

Design overview

Akka persistence query is purposely designed to be a very loosely specified API. This is in order to keep the provided APIs general enough for each journal implementation to be able to expose its best features, e.g. a SQL journal can use complex SQL queries or if a journal is able to subscribe to a live event stream this should also be possible to expose the same API - a typed stream of events.

Each read journal must explicitly document which types of queries it supports. Refer to your journal's plugins documentation for details on which queries and semantics it supports.

While Akka Persistence Query does not provide actual implementations of ReadJournals, it defines a number of pre-defined query types for the most common query scenarios, that most journals are likely to implement (however they are not required to).

Read Journals

In order to issue queries one has to first obtain an instance of a ReadJournal. Read journals are implemented asCommunity plugins, each targeting a specific datastore (for example Cassandra or JDBC databases). For example, given a library that provides a akka.persistence.query.my-read-journal obtaining the related journal is as simple as:

  1. // obtain read journal by plugin id
  2. val readJournal =
  3. PersistenceQuery(system).readJournalFor[MyScaladslReadJournal](
  4. "akka.persistence.query.my-read-journal")
  5.  
  6. // issue query to journal
  7. val source: Source[EventEnvelope, NotUsed] =
  8. readJournal.eventsByPersistenceId("user-1337", 0, Long.MaxValue)
  9.  
  10. // materialize stream, consuming events
  11. implicit val mat = ActorMaterializer()
  12. source.runForeach { event => println("Event: " + event) }

Journal implementers are encouraged to put this identifier in a variable known to the user, such that one can access it via readJournalFor[NoopJournal](NoopJournal.identifier), however this is not enforced.

Read journal implementations are available as Community plugins.

Predefined queries

Akka persistence query comes with a number of query interfaces built in and suggests Journal implementors to implement them according to the semantics described below. It is important to notice that while these query types are very common a journal is not obliged to implement all of them - for example because in a given journal such query would be significantly inefficient.

Note

Refer to the documentation of the ReadJournal plugin you are using for a specific list of supported query types. For example, Journal plugins should document their stream completion strategies.

The predefined queries are:

AllPersistenceIdsQuery and CurrentPersistenceIdsQuery

allPersistenceIds which is designed to allow users to subscribe to a stream of all persistent ids in the system. By default this stream should be assumed to be a "live" stream, which means that the journal should keep emitting new persistence ids as they come into the system:

  1. readJournal.allPersistenceIds()

If your usage does not require a live stream, you can use the currentPersistenceIds query:

  1. readJournal.currentPersistenceIds()

EventsByPersistenceIdQuery and CurrentEventsByPersistenceIdQuery

eventsByPersistenceId is a query equivalent to replaying a PersistentActor, however, since it is a stream it is possible to keep it alive and watch for additional incoming events persisted by the persistent actor identified by the given persistenceId.

  1. readJournal.eventsByPersistenceId("user-us-1337")

Most journals will have to revert to polling in order to achieve this, which can typically be configured with a refresh-interval configuration property.

If your usage does not require a live stream, you can use the currentEventsByPersistenceId query.

EventsByTag and CurrentEventsByTag

eventsByTag allows querying events regardless of which persistenceId they are associated with. This query is hard to implement in some journals or may need some additional preparation of the used data store to be executed efficiently. The goal of this query is to allow querying for all events which are "tagged" with a specific tag. That includes the use case to query all domain events of an Aggregate Root type. Please refer to your read journal plugin's documentation to find out if and how it is supported.

Some journals may support tagging of events via an Event Adapters that wraps the events in aakka.persistence.journal.Tagged with the given tags. The journal may support other ways of doing tagging - again, how exactly this is implemented depends on the used journal. Here is an example of such a tagging event adapter:

  1. import akka.persistence.journal.WriteEventAdapter
  2. import akka.persistence.journal.Tagged
  3.  
  4. class MyTaggingEventAdapter extends WriteEventAdapter {
  5. val colors = Set("green", "black", "blue")
  6. override def toJournal(event: Any): Any = event match {
  7. case s: String =>
  8. var tags = colors.foldLeft(Set.empty[String]) { (acc, c) =>
  9. if (s.contains(c)) acc + c else acc
  10. }
  11. if (tags.isEmpty) event
  12. else Tagged(event, tags)
  13. case _ => event
  14. }
  15.  
  16. override def manifest(event: Any): String = ""
  17. }

Note

A very important thing to keep in mind when using queries spanning multiple persistenceIds, such asEventsByTag is that the order of events at which the events appear in the stream rarely is guaranteed (or stable between materializations).

Journals may choose to opt for strict ordering of the events, and should then document explicitly what kind of ordering guarantee they provide - for example "ordered by timestamp ascending, independently of persistenceId" is easy to achieve on relational databases, yet may be hard to implement efficiently on plain key-value datastores.

In the example below we query all events which have been tagged (we assume this was performed by the write-side using an EventAdapter, or that the journal is smart enough that it can figure out what we mean by this tag - for example if the journal stored the events as json it may try to find those with the field tag set to this value etc.).

  1. // assuming journal is able to work with numeric offsets we can:
  2.  
  3. val blueThings: Source[EventEnvelope, NotUsed] =
  4. readJournal.eventsByTag("blue")
  5.  
  6. // find top 10 blue things:
  7. val top10BlueThings: Future[Vector[Any]] =
  8. blueThings
  9. .map(_.event)
  10. .take(10) // cancels the query stream after pulling 10 elements
  11. .runFold(Vector.empty[Any])(_ :+ _)
  12.  
  13. // start another query, from the known offset
  14. val furtherBlueThings = readJournal.eventsByTag("blue", offset = 10)

As you can see, we can use all the usual stream combinators available from Akka Streams on the resulting query stream, including for example taking the first 10 and cancelling the stream. It is worth pointing out that the built-inEventsByTag query has an optionally supported offset parameter (of type Long) which the journals can use to implement resumable-streams. For example a journal may be able to use a WHERE clause to begin the read starting from a specific row, or in a datastore that is able to order events by insertion time it could treat the Long as a timestamp and select only older events.

If your usage does not require a live stream, you can use the currentEventsByTag query.

Materialized values of queries

Journals are able to provide additional information related to a query by exposing materialized values, which are a feature of Akka Streams that allows to expose additional values at stream materialization time.

More advanced query journals may use this technique to expose information about the character of the materialized stream, for example if it's finite or infinite, strictly ordered or not ordered at all. The materialized value type is defined as the second type parameter of the returned Source, which allows journals to provide users with their specialised query object, as demonstrated in the sample below:

  1. final case class RichEvent(tags: Set[String], payload: Any)
  2.  
  3. // a plugin can provide:
  4. case class QueryMetadata(deterministicOrder: Boolean, infinite: Boolean)
  1. def byTagsWithMeta(tags: Set[String]): Source[RichEvent, QueryMetadata] = {
  1. val query: Source[RichEvent, QueryMetadata] =
  2. readJournal.byTagsWithMeta(Set("red", "blue"))
  3.  
  4. query
  5. .mapMaterializedValue { meta =>
  6. println(s"The query is: " +
  7. s"ordered deterministically: ${meta.deterministicOrder}, " +
  8. s"infinite: ${meta.infinite}")
  9. }
  10. .map { event => println(s"Event payload: ${event.payload}") }
  11. .runWith(Sink.ignore)

Performance and denormalization

When building systems using Event sourcing and CQRS (Command & Query Responsibility Segregation) techniques it is tremendously important to realise that the write-side has completely different needs from the read-side, and separating those concerns into datastores that are optimised for either side makes it possible to offer the best experience for the write and read sides independently.

For example, in a bidding system it is important to "take the write" and respond to the bidder that we have accepted the bid as soon as possible, which means that write-throughput is of highest importance for the write-side – often this means that data stores which are able to scale to accommodate these requirements have a less expressive query side.

On the other hand the same application may have some complex statistics view or we may have analysts working with the data to figure out best bidding strategies and trends – this often requires some kind of expressive query capabilities like for example SQL or writing Spark jobs to analyse the data. Therefore the data stored in the write-side needs to be projected into the other read-optimised datastore.

Note

When referring to Materialized Views in Akka Persistence think of it as "some persistent storage of the result of a Query". In other words, it means that the view is created once, in order to be afterwards queried multiple times, as in this format it may be more efficient or interesting to query it (instead of the source events directly).

Materialize view to Reactive Streams compatible datastore

If the read datastore exposes a Reactive Streams interface then implementing a simple projection is as simple as, using the read-journal and feeding it into the databases driver interface, for example like so:

  1. implicit val system = ActorSystem()
  2. implicit val mat = ActorMaterializer()
  3.  
  4. val readJournal =
  5. PersistenceQuery(system).readJournalFor[MyScaladslReadJournal](JournalId)
  6. val dbBatchWriter: Subscriber[immutable.Seq[Any]] =
  7. ReactiveStreamsCompatibleDBDriver.batchWriter
  8.  
  9. // Using an example (Reactive Streams) Database driver
  10. readJournal
  11. .eventsByPersistenceId("user-1337")
  12. .map(envelope => envelope.event)
  13. .map(convertToReadSideTypes) // convert to datatype
  14. .grouped(20) // batch inserts into groups of 20
  15. .runWith(Sink.fromSubscriber(dbBatchWriter)) // write batches to read-side database

Materialize view using mapAsync

If the target database does not provide a reactive streams Subscriber that can perform writes, you may have to implement the write logic using plain functions or Actors instead.

In case your write logic is state-less and you just need to convert the events from one data type to another before writing into the alternative datastore, then the projection is as simple as:

  1. trait ExampleStore {
  2. def save(event: Any): Future[Unit]
  3. }
  4. val store: ExampleStore = ???
  5.  
  6. readJournal
  7. .eventsByTag("bid")
  8. .mapAsync(1) { e => store.save(e) }
  9. .runWith(Sink.ignore)

Resumable projections

Sometimes you may need to implement "resumable" projections, that will not start from the beginning of time each time when run. In this case you will need to store the sequence number (or offset) of the processed event and use it the next time this projection is started. This pattern is not built-in, however is rather simple to implement yourself.

The example below additionally highlights how you would use Actors to implement the write side, in case you need to do some complex logic that would be best handled inside an Actor before persisting the event into the other datastore:

  1. import akka.pattern.ask
  2. import system.dispatcher
  3. implicit val timeout = Timeout(3.seconds)
  4.  
  5. val bidProjection = new MyResumableProjection("bid")
  6.  
  7. val writerProps = Props(classOf[TheOneWhoWritesToQueryJournal], "bid")
  8. val writer = system.actorOf(writerProps, "bid-projection-writer")
  9.  
  10. bidProjection.latestOffset.foreach { startFromOffset =>
  11. readJournal
  12. .eventsByTag("bid", startFromOffset)
  13. .mapAsync(8) { envelope => (writer ? envelope.event).map(_ => envelope.offset) }
  14. .mapAsync(1) { offset => bidProjection.saveProgress(offset) }
  15. .runWith(Sink.ignore)
  16. }
  1. class TheOneWhoWritesToQueryJournal(id: String) extends Actor {
  2. val store = new DummyStore()
  3.  
  4. var state: ComplexState = ComplexState()
  5.  
  6. def receive = {
  7. case m =>
  8. state = updateState(state, m)
  9. if (state.readyToSave) store.save(Record(state))
  10. }
  11.  
  12. def updateState(state: ComplexState, msg: Any): ComplexState = {
  13. // some complicated aggregation logic here ...
  14. state
  15. }
  16. }

Query plugins

Query plugins are various (mostly community driven) ReadJournal implementations for all kinds of available datastores. The complete list of available plugins is maintained on the Akka Persistence Query Community Plugins page.

The plugin for LevelDB is described in Persistence Query for LevelDB.

This section aims to provide tips and guide plugin developers through implementing a custom query plugin. Most users will not need to implement journals themselves, except if targeting a not yet supported datastore.

Note

Since different data stores provide different query capabilities journal plugins must extensively document their exposed semantics as well as handled query scenarios.

ReadJournal plugin API

A read journal plugin must implement akka.persistence.query.ReadJournalProvider which creates instances ofakka.persistence.query.scaladsl.ReadJournal and akka.persistence.query.javaadsl.ReadJournal. The plugin must implement both the scaladsl and the javadsl traits because the akka.stream.scaladsl.Sourceand akka.stream.javadsl.Source are different types and even though those types can easily be converted to each other it is most convenient for the end user to get access to the Java or Scala directly. As illustrated below one of the implementations can delegate to the other.

Below is a simple journal implementation:

  1. class MyReadJournalProvider(system: ExtendedActorSystem, config: Config)
  2. extends ReadJournalProvider {
  3.  
  4. override val scaladslReadJournal: MyScaladslReadJournal =
  5. new MyScaladslReadJournal(system, config)
  6.  
  7. override val javadslReadJournal: MyJavadslReadJournal =
  8. new MyJavadslReadJournal(scaladslReadJournal)
  9. }
  10.  
  11. class MyScaladslReadJournal(system: ExtendedActorSystem, config: Config)
  12. extends akka.persistence.query.scaladsl.ReadJournal
  13. with akka.persistence.query.scaladsl.EventsByTagQuery
  14. with akka.persistence.query.scaladsl.EventsByPersistenceIdQuery
  15. with akka.persistence.query.scaladsl.AllPersistenceIdsQuery
  16. with akka.persistence.query.scaladsl.CurrentPersistenceIdsQuery {
  17.  
  18. private val refreshInterval: FiniteDuration =
  19. config.getDuration("refresh-interval", MILLISECONDS).millis
  20.  
  21. override def eventsByTag(
  22. tag: String, offset: Long = 0L): Source[EventEnvelope, NotUsed] = {
  23. val props = MyEventsByTagPublisher.props(tag, offset, refreshInterval)
  24. Source.actorPublisher[EventEnvelope](props)
  25. .mapMaterializedValue(_ => NotUsed)
  26. }
  27.  
  28. override def eventsByPersistenceId(
  29. persistenceId: String, fromSequenceNr: Long = 0L,
  30. toSequenceNr: Long = Long.MaxValue): Source[EventEnvelope, NotUsed] = {
  31. // implement in a similar way as eventsByTag
  32. ???
  33. }
  34.  
  35. override def allPersistenceIds(): Source[String, NotUsed] = {
  36. // implement in a similar way as eventsByTag
  37. ???
  38. }
  39.  
  40. override def currentPersistenceIds(): Source[String, NotUsed] = {
  41. // implement in a similar way as eventsByTag
  42. ???
  43. }
  44.  
  45. // possibility to add more plugin specific queries
  46.  
  47. def byTagsWithMeta(tags: Set[String]): Source[RichEvent, QueryMetadata] = {
  48. // implement in a similar way as eventsByTag
  49. ???
  50. }
  51.  
  52. }
  53.  
  54. class MyJavadslReadJournal(scaladslReadJournal: MyScaladslReadJournal)
  55. extends akka.persistence.query.javadsl.ReadJournal
  56. with akka.persistence.query.javadsl.EventsByTagQuery
  57. with akka.persistence.query.javadsl.EventsByPersistenceIdQuery
  58. with akka.persistence.query.javadsl.AllPersistenceIdsQuery
  59. with akka.persistence.query.javadsl.CurrentPersistenceIdsQuery {
  60.  
  61. override def eventsByTag(
  62. tag: String, offset: Long = 0L): javadsl.Source[EventEnvelope, NotUsed] =
  63. scaladslReadJournal.eventsByTag(tag, offset).asJava
  64.  
  65. override def eventsByPersistenceId(
  66. persistenceId: String, fromSequenceNr: Long = 0L,
  67. toSequenceNr: Long = Long.MaxValue): javadsl.Source[EventEnvelope, NotUsed] =
  68. scaladslReadJournal.eventsByPersistenceId(
  69. persistenceId, fromSequenceNr, toSequenceNr).asJava
  70.  
  71. override def allPersistenceIds(): javadsl.Source[String, NotUsed] =
  72. scaladslReadJournal.allPersistenceIds().asJava
  73.  
  74. override def currentPersistenceIds(): javadsl.Source[String, NotUsed] =
  75. scaladslReadJournal.currentPersistenceIds().asJava
  76.  
  77. // possibility to add more plugin specific queries
  78.  
  79. def byTagsWithMeta(
  80. tags: java.util.Set[String]): javadsl.Source[RichEvent, QueryMetadata] = {
  81. import scala.collection.JavaConverters._
  82. scaladslReadJournal.byTagsWithMeta(tags.asScala.toSet).asJava
  83. }
  84. }

And the eventsByTag could be backed by such an Actor for example:

  1. class MyEventsByTagPublisher(tag: String, offset: Long, refreshInterval: FiniteDuration)
  2. extends ActorPublisher[EventEnvelope] {
  3.  
  4. private case object Continue
  5.  
  6. private val connection: java.sql.Connection = ???
  7.  
  8. private val Limit = 1000
  9. private var currentOffset = offset
  10. var buf = Vector.empty[EventEnvelope]
  11.  
  12. import context.dispatcher
  13. val continueTask = context.system.scheduler.schedule(
  14. refreshInterval, refreshInterval, self, Continue)
  15.  
  16. override def postStop(): Unit = {
  17. continueTask.cancel()
  18. }
  19.  
  20. def receive = {
  21. case _: Request | Continue =>
  22. query()
  23. deliverBuf()
  24.  
  25. case Cancel =>
  26. context.stop(self)
  27. }
  28.  
  29. object Select {
  30. private def statement() = connection.prepareStatement(
  31. """
  32. SELECT id, persistent_repr FROM journal
  33. WHERE tag = ? AND id >= ?
  34. ORDER BY id LIMIT ?
  35. """)
  36.  
  37. def run(tag: String, from: Long, limit: Int): Vector[(Long, Array[Byte])] = {
  38. val s = statement()
  39. try {
  40. s.setString(1, tag)
  41. s.setLong(2, from)
  42. s.setLong(3, limit)
  43. val rs = s.executeQuery()
  44.  
  45. val b = Vector.newBuilder[(Long, Array[Byte])]
  46. while (rs.next())
  47. b += (rs.getLong(1) -> rs.getBytes(2))
  48. b.result()
  49. } finally s.close()
  50. }
  51. }
  52.  
  53. def query(): Unit =
  54. if (buf.isEmpty) {
  55. try {
  56. val result = Select.run(tag, currentOffset, Limit)
  57. currentOffset = if (result.nonEmpty) result.last._1 else currentOffset
  58. val serialization = SerializationExtension(context.system)
  59.  
  60. buf = result.map {
  61. case (id, bytes) =>
  62. val p = serialization.deserialize(bytes, classOf[PersistentRepr]).get
  63. EventEnvelope(offset = id, p.persistenceId, p.sequenceNr, p.payload)
  64. }
  65. } catch {
  66. case e: Exception =>
  67. onErrorThenStop(e)
  68. }
  69. }
  70.  
  71. final def deliverBuf(): Unit =
  72. if (totalDemand > 0 && buf.nonEmpty) {
  73. if (totalDemand <= Int.MaxValue) {
  74. val (use, keep) = buf.splitAt(totalDemand.toInt)
  75. buf = keep
  76. use foreach onNext
  77. } else {
  78. buf foreach onNext
  79. buf = Vector.empty
  80. }
  81. }
  82. }

If the underlying datastore only supports queries that are completed when they reach the end of the "result set", the journal has to submit new queries after a while in order to support "infinite" event streams that include events stored after the initial query has completed. It is recommended that the plugin use a configuration property named refresh-interval for defining such a refresh interval.

Plugin TCK

TODO, not available yet.

Persistence Query for LevelDB

This is documentation for the LevelDB implementation of the Persistence Query API. Note that implementations for other journals may have different semantics.

Warning

This module is marked as “experimental” as of its introduction in Akka 2.4.0. We will continue to improve this API based on our users’ feedback, which implies that while we try to keep incompatible changes to a minimum the binary compatibility guarantee for maintenance releases does not apply to the contents of theakka.persistence.query package.

Dependencies

Akka persistence LevelDB query implementation is bundled in the akka-persistence-query-experimental artifact. Make sure that you have the following dependency in your project:

  1. "com.typesafe.akka" %% "akka-persistence-query-experimental" % "2.4.9"

How to get the ReadJournal

The ReadJournal is retrieved via the akka.persistence.query.PersistenceQuery extension:

  1. import akka.persistence.query.PersistenceQuery
  2. import akka.persistence.query.journal.leveldb.scaladsl.LeveldbReadJournal
  3.  
  4. val queries = PersistenceQuery(system).readJournalFor[LeveldbReadJournal](
  5. LeveldbReadJournal.Identifier)

Supported Queries

EventsByPersistenceIdQuery and CurrentEventsByPersistenceIdQuery

eventsByPersistenceId is used for retrieving events for a specific PersistentActor identified bypersistenceId.

  1. implicit val mat = ActorMaterializer()(system)
  2. val queries = PersistenceQuery(system).readJournalFor[LeveldbReadJournal](
  3. LeveldbReadJournal.Identifier)
  4.  
  5. val src: Source[EventEnvelope, NotUsed] =
  6. queries.eventsByPersistenceId("some-persistence-id", 0L, Long.MaxValue)
  7.  
  8. val events: Source[Any, NotUsed] = src.map(_.event)

You can retrieve a subset of all events by specifying fromSequenceNr and toSequenceNr or use 0L andLong.MaxValue respectively to retrieve all events. Note that the corresponding sequence number of each event is provided in the EventEnvelope, which makes it possible to resume the stream at a later point from a given sequence number.

The returned event stream is ordered by sequence number, i.e. the same order as the PersistentActor persisted the events. The same prefix of stream elements (in same order) are returned for multiple executions of the query, except for when events have been deleted.

The stream is not completed when it reaches the end of the currently stored events, but it continues to push new events when new events are persisted. Corresponding query that is completed when it reaches the end of the currently stored events is provided by currentEventsByPersistenceId.

The LevelDB write journal is notifying the query side as soon as events are persisted, but for efficiency reasons the query side retrieves the events in batches that sometimes can be delayed up to the configured refresh-interval or givenRefreshInterval hint.

The stream is completed with failure if there is a failure in executing the query in the backend journal.

AllPersistenceIdsQuery and CurrentPersistenceIdsQuery

allPersistenceIds is used for retrieving all persistenceIds of all persistent actors.

  1. implicit val mat = ActorMaterializer()(system)
  2. val queries = PersistenceQuery(system).readJournalFor[LeveldbReadJournal](
  3. LeveldbReadJournal.Identifier)
  4.  
  5. val src: Source[String, NotUsed] = queries.allPersistenceIds()

The returned event stream is unordered and you can expect different order for multiple executions of the query.

The stream is not completed when it reaches the end of the currently used persistenceIds, but it continues to push newpersistenceIds when new persistent actors are created. Corresponding query that is completed when it reaches the end of the currently used persistenceIds is provided by currentPersistenceIds.

The LevelDB write journal is notifying the query side as soon as new persistenceIds are created and there is no periodic polling or batching involved in this query.

The stream is completed with failure if there is a failure in executing the query in the backend journal.

EventsByTag and CurrentEventsByTag

eventsByTag is used for retrieving events that were marked with a given tag, e.g. all domain events of an Aggregate Root type.

  1. implicit val mat = ActorMaterializer()(system)
  2. val queries = PersistenceQuery(system).readJournalFor[LeveldbReadJournal](
  3. LeveldbReadJournal.Identifier)
  4.  
  5. val src: Source[EventEnvelope, NotUsed] =
  6. queries.eventsByTag(tag = "green", offset = 0L)

To tag events you create an Event Adapters that wraps the events in a akka.persistence.journal.Tagged with the given tags.

  1. import akka.persistence.journal.WriteEventAdapter
  2. import akka.persistence.journal.Tagged
  3.  
  4. class MyTaggingEventAdapter extends WriteEventAdapter {
  5. val colors = Set("green", "black", "blue")
  6. override def toJournal(event: Any): Any = event match {
  7. case s: String =>
  8. var tags = colors.foldLeft(Set.empty[String]) { (acc, c) =>
  9. if (s.contains(c)) acc + c else acc
  10. }
  11. if (tags.isEmpty) event
  12. else Tagged(event, tags)
  13. case _ => event
  14. }
  15.  
  16. override def manifest(event: Any): String = ""
  17. }

You can retrieve a subset of all events by specifying offset, or use 0L to retrieve all events with a given tag. Theoffset corresponds to an ordered sequence number for the specific tag. Note that the corresponding offset of each event is provided in the EventEnvelope, which makes it possible to resume the stream at a later point from a given offset.

In addition to the offset the EventEnvelope also provides persistenceId and sequenceNr for each event. ThesequenceNr is the sequence number for the persistent actor with the persistenceId that persisted the event. ThepersistenceId + sequenceNr is an unique identifier for the event.

The returned event stream is ordered by the offset (tag sequence number), which corresponds to the same order as the write journal stored the events. The same stream elements (in same order) are returned for multiple executions of the query. Deleted events are not deleted from the tagged event stream.

Note

Events deleted using deleteMessages(toSequenceNr) are not deleted from the "tagged stream".

The stream is not completed when it reaches the end of the currently stored events, but it continues to push new events when new events are persisted. Corresponding query that is completed when it reaches the end of the currently stored events is provided by currentEventsByTag.

The LevelDB write journal is notifying the query side as soon as tagged events are persisted, but for efficiency reasons the query side retrieves the events in batches that sometimes can be delayed up to the configured refresh-intervalor given RefreshInterval hint.

The stream is completed with failure if there is a failure in executing the query in the backend journal.

Configuration

Configuration settings can be defined in the configuration section with the absolute path corresponding to the identifier, which is "akka.persistence.query.journal.leveldb" for the default LeveldbReadJournal.Identifier.

It can be configured with the following properties:

  1. # Configuration for the LeveldbReadJournal
  2. akka.persistence.query.journal.leveldb {
  3. # Implementation class of the LevelDB ReadJournalProvider
  4. class = "akka.persistence.query.journal.leveldb.LeveldbReadJournalProvider"
  5. # Absolute path to the write journal plugin configuration entry that this
  6. # query journal will connect to. That must be a LeveldbJournal or SharedLeveldbJournal.
  7. # If undefined (or "") it will connect to the default journal as specified by the
  8. # akka.persistence.journal.plugin property.
  9. write-plugin = ""
  10. # The LevelDB write journal is notifying the query side as soon as things
  11. # are persisted, but for efficiency reasons the query side retrieves the events
  12. # in batches that sometimes can be delayed up to the configured `refresh-interval`.
  13. refresh-interval = 3s
  14. # How many events to fetch in one query (replay) and keep buffered until they
  15. # are delivered downstreams.
  16. max-buffer-size = 100
  17. }

Testing Actor Systems

As with any piece of software, automated tests are a very important part of the development cycle. The actor model presents a different view on how units of code are delimited and how they interact, which has an influence on how to perform tests.

Akka comes with a dedicated module akka-testkit for supporting tests at different levels, which fall into two clearly distinct categories:

There are of course variations on the granularity of tests in both categories, where unit testing reaches down to white-box tests and integration testing can encompass functional tests of complete actor networks. The important distinction lies in whether concurrency concerns are part of the test or not. The tools offered are described in detail in the following sections.

Note

Be sure to add the module akka-testkit to your dependencies.

Synchronous Unit Testing with TestActorRef

Testing the business logic inside Actor classes can be divided into two parts: first, each atomic operation must work in isolation, then sequences of incoming events must be processed correctly, even in the presence of some possible variability in the ordering of events. The former is the primary use case for single-threaded unit testing, while the latter can only be verified in integration tests.

Normally, the ActorRef shields the underlying Actor instance from the outside, the only communications channel is the actor's mailbox. This restriction is an impediment to unit testing, which led to the inception of the TestActorRef. This special type of reference is designed specifically for test purposes and allows access to the actor in two ways: either by obtaining a reference to the underlying actor instance, or by invoking or querying the actor's behaviour (receive). Each one warrants its own section below.

Note

It is highly recommended to stick to traditional behavioural testing (using messaging to ask the Actor to reply with the state you want to run assertions against), instead of using TestActorRef whenever possible.

Warning

Due to the synchronous nature of TestActorRef it will not work with some support traits that Akka provides as they require asynchronous behaviours to function properly. Examples of traits that do not mix well with test actor refs are PersistentActor and AtLeastOnceDelivery provided by Akka Persistence.

Obtaining a Reference to an Actor

Having access to the actual Actor object allows application of all traditional unit testing techniques on the contained methods. Obtaining a reference is done like this:

  1. import akka.testkit.TestActorRef
  2.  
  3. val actorRef = TestActorRef[MyActor]
  4. val actor = actorRef.underlyingActor

Since TestActorRef is generic in the actor type it returns the underlying actor with its proper static type. From this point on you may bring any unit testing tool to bear on your actor as usual.

Testing Finite State Machines

If your actor under test is a FSM, you may use the special TestFSMRef which offers all features of a normalTestActorRef and in addition allows access to the internal state:

  1. import akka.testkit.TestFSMRef
  2. import akka.actor.FSM
  3. import scala.concurrent.duration._
  4.  
  5. val fsm = TestFSMRef(new TestFsmActor)
  6.  
  7. val mustBeTypedProperly: TestActorRef[TestFsmActor] = fsm
  8.  
  9. assert(fsm.stateName == 1)
  10. assert(fsm.stateData == "")
  11. fsm ! "go" // being a TestActorRef, this runs also on the CallingThreadDispatcher
  12. assert(fsm.stateName == 2)
  13. assert(fsm.stateData == "go")
  14.  
  15. fsm.setState(stateName = 1)
  16. assert(fsm.stateName == 1)
  17.  
  18. assert(fsm.isTimerActive("test") == false)
  19. fsm.setTimer("test", 12, 10 millis, true)
  20. assert(fsm.isTimerActive("test") == true)
  21. fsm.cancelTimer("test")
  22. assert(fsm.isTimerActive("test") == false)

Due to a limitation in Scala’s type inference, there is only the factory method shown above, so you will probably write code like TestFSMRef(new MyFSM) instead of the hypothetical ActorRef-inspired TestFSMRef[MyFSM]. All methods shown above directly access the FSM state without any synchronization; this is perfectly alright if theCallingThreadDispatcher is used and no other threads are involved, but it may lead to surprises if you were to actually exercise timer events, because those are executed on the Scheduler thread.

Testing the Actor's Behavior

When the dispatcher invokes the processing behavior of an actor on a message, it actually calls apply on the current behavior registered for the actor. This starts out with the return value of the declared receive method, but it may also be changed using become and unbecome in response to external messages. All of this contributes to the overall actor behavior and it does not lend itself to easy testing on the Actor itself. Therefore the TestActorRef offers a different mode of operation to complement the Actor testing: it supports all operations also valid on normal ActorRef. Messages sent to the actor are processed synchronously on the current thread and answers may be sent back as usual. This trick is made possible by the CallingThreadDispatcher described below (see CallingThreadDispatcher); this dispatcher is set implicitly for any actor instantiated into a TestActorRef.

  1. import akka.testkit.TestActorRef
  2. import scala.concurrent.duration._
  3. import scala.concurrent.Await
  4. import akka.pattern.ask
  5.  
  6. val actorRef = TestActorRef(new MyActor)
  7. // hypothetical message stimulating a '42' answer
  8. val future = actorRef ? Say42
  9. val Success(result: Int) = future.value.get
  10. result should be(42)

As the TestActorRef is a subclass of LocalActorRef with a few special extras, also aspects like supervision and restarting work properly, but beware that execution is only strictly synchronous as long as all actors involved use theCallingThreadDispatcher. As soon as you add elements which include more sophisticated scheduling you leave the realm of unit testing as you then need to think about asynchronicity again (in most cases the problem will be to wait until the desired effect had a chance to happen).

One more special aspect which is overridden for single-threaded tests is the receiveTimeout, as including that would entail asynchronous queuing of ReceiveTimeout messages, violating the synchronous contract.

Note

To summarize: TestActorRef overwrites two fields: it sets the dispatcher toCallingThreadDispatcher.global and it sets the receiveTimeout to None.

The Way In-Between: Expecting Exceptions

If you want to test the actor behavior, including hotswapping, but without involving a dispatcher and without having theTestActorRef swallow any thrown exceptions, then there is another mode available for you: just use the receivemethod on TestActorRef, which will be forwarded to the underlying actor:

  1. import akka.testkit.TestActorRef
  2.  
  3. val actorRef = TestActorRef(new Actor {
  4. def receive = {
  5. case "hello" => throw new IllegalArgumentException("boom")
  6. }
  7. })
  8. intercept[IllegalArgumentException] { actorRef.receive("hello") }

Use Cases

You may of course mix and match both modi operandi of TestActorRef as suits your test needs:

  • one common use case is setting up the actor into a specific internal state before sending the test message
  • another is to verify correct internal state transitions after having sent the test message

Feel free to experiment with the possibilities, and if you find useful patterns, don't hesitate to let the Akka forums know about them! Who knows, common operations might even be worked into nice DSLs.

Asynchronous Integration Testing with TestKit

When you are reasonably sure that your actor's business logic is correct, the next step is verifying that it works correctly within its intended environment (if the individual actors are simple enough, possibly because they use the FSMmodule, this might also be the first step). The definition of the environment depends of course very much on the problem at hand and the level at which you intend to test, ranging for functional/integration tests to full system tests. The minimal setup consists of the test procedure, which provides the desired stimuli, the actor under test, and an actor receiving replies. Bigger systems replace the actor under test with a network of actors, apply stimuli at varying injection points and arrange results to be sent from different emission points, but the basic principle stays the same in that a single procedure drives the test.

The TestKit class contains a collection of tools which makes this common task easy.

  1. import akka.actor.ActorSystem
  2. import akka.actor.Actor
  3. import akka.actor.Props
  4. import akka.testkit.{ TestActors, TestKit, ImplicitSender }
  5. import org.scalatest.WordSpecLike
  6. import org.scalatest.Matchers
  7. import org.scalatest.BeforeAndAfterAll
  8.  
  9. class MySpec() extends TestKit(ActorSystem("MySpec")) with ImplicitSender
  10. with WordSpecLike with Matchers with BeforeAndAfterAll {
  11.  
  12. override def afterAll {
  13. TestKit.shutdownActorSystem(system)
  14. }
  15.  
  16. "An Echo actor" must {
  17.  
  18. "send back messages unchanged" in {
  19. val echo = system.actorOf(TestActors.echoActorProps)
  20. echo ! "hello world"
  21. expectMsg("hello world")
  22. }
  23.  
  24. }
  25. }

The TestKit contains an actor named testActor which is the entry point for messages to be examined with the various expectMsg... assertions detailed below. When mixing in the trait ImplicitSender this test actor is implicitly used as sender reference when dispatching messages from the test procedure. The testActor may also be passed to other actors as usual, usually subscribing it as notification listener. There is a whole set of examination methods, e.g. receiving all consecutive messages matching certain criteria, receiving a whole sequence of fixed messages or classes, receiving nothing for some time, etc.

The ActorSystem passed in to the constructor of TestKit is accessible via the system member. Remember to shut down the actor system after the test is finished (also in case of failure) so that all actors—including the test actor—are stopped.

Built-In Assertions

The above mentioned expectMsg is not the only method for formulating assertions concerning received messages. Here is the full list:

  • expectMsg[T](d: Duration, msg: T): T

    The given message object must be received within the specified time; the object will be returned.

  • expectMsgPF[T](d: Duration)(pf: PartialFunction[Any, T]): T

    Within the given time period, a message must be received and the given partial function must be defined for that message; the result from applying the partial function to the received message is returned. The duration may be left unspecified (empty parentheses are required in this case) to use the deadline from the innermost enclosing within block instead.

  • expectMsgClass[T](d: Duration, c: Class[T]): T

    An object which is an instance of the given Class must be received within the allotted time frame; the object will be returned. Note that this does a conformance check; if you need the class to be equal, have a look at expectMsgAllClassOf with a single given class argument.

  • expectMsgType[T: Manifest](d: Duration)

    An object which is an instance of the given type (after erasure) must be received within the allotted time frame; the object will be returned. This method is approximately equivalent toexpectMsgClass(implicitly[ClassTag[T]].runtimeClass).

  • expectMsgAnyOf[T](d: Duration, obj: T*): T

    An object must be received within the given time, and it must be equal ( compared with ==) to at least one of the passed reference objects; the received object will be returned.

  • expectMsgAnyClassOf[T](d: Duration, obj: Class[_ <: T]*): T

    An object must be received within the given time, and it must be an instance of at least one of the supplied Class objects; the received object will be returned.

  • expectMsgAllOf[T](d: Duration, obj: T*): Seq[T]

    A number of objects matching the size of the supplied object array must be received within the given time, and for each of the given objects there must exist at least one among the received ones which equals (compared with ==) it. The full sequence of received objects is returned.

  • expectMsgAllClassOf[T](d: Duration, c: Class[_ <: T]*): Seq[T]

    A number of objects matching the size of the supplied Class array must be received within the given time, and for each of the given classes there must exist at least one among the received objects whose class equals (compared with ==) it (this is not a conformance check). The full sequence of received objects is returned.

  • expectMsgAllConformingOf[T](d: Duration, c: Class[_ <: T]*): Seq[T]

    A number of objects matching the size of the supplied Class array must be received within the given time, and for each of the given classes there must exist at least one among the received objects which is an instance of this class. The full sequence of received objects is returned.

  • expectNoMsg(d: Duration)

    No message must be received within the given time. This also fails if a message has been received before calling this method which has not been removed from the queue using one of the other methods.

  • receiveN(n: Int, d: Duration): Seq[AnyRef]

    n messages must be received within the given time; the received messages are returned.

  • fishForMessage(max: Duration, hint: String)(pf: PartialFunction[Any, Boolean]): Any

    Keep receiving messages as long as the time is not used up and the partial function matches and returns false. Returns the message received for which it returned true or throws an exception, which will include the provided hint for easier debugging.

In addition to message reception assertions there are also methods which help with message flows:

  • receiveOne(d: Duration): AnyRef

    Tries to receive one message for at most the given time interval and returns null in case of failure. If the given Duration is zero, the call is non-blocking (polling mode).

  • receiveWhile[T](max: Duration, idle: Duration, messages: Int)(pf: PartialFunction[Any, T]): Seq[T]

    Collect messages as long as

    • they are matching the given partial function
    • the given time interval is not used up
    • the next message is received within the idle timeout
    • the number of messages has not yet reached the maximum

    All collected messages are returned. The maximum duration defaults to the time remaining in the innermost enclosing within block and the idle duration defaults to infinity (thereby disabling the idle timeout feature). The number of expected messages defaults to Int.MaxValue, which effectively disables this limit.

  • awaitCond(p: => Boolean, max: Duration, interval: Duration)

    Poll the given condition every interval until it returns true or the max duration is used up. The interval defaults to 100 ms and the maximum defaults to the time remaining in the innermost enclosing within block.

  • awaitAssert(a: => Any, max: Duration, interval: Duration)

    Poll the given assert function every interval until it does not throw an exception or the maxduration is used up. If the timeout expires the last exception is thrown. The interval defaults to 100 ms and the maximum defaults to the time remaining in the innermost enclosing within block.The interval defaults to 100 ms and the maximum defaults to the time remaining in the innermost enclosing withinblock.

  • ignoreMsg(pf: PartialFunction[AnyRef, Boolean])

    ignoreNoMsg

    The internal testActor contains a partial function for ignoring messages: it will only enqueue messages which do not match the function or for which the function returns false. This function can be set and reset using the methods given above; each invocation replaces the previous function, they are not composed.

    This feature is useful e.g. when testing a logging system, where you want to ignore regular messages and are only interested in your specific ones.

Expecting Log Messages

Since an integration test does not allow to the internal processing of the participating actors, verifying expected exceptions cannot be done directly. Instead, use the logging system for this purpose: replacing the normal event handler with the TestEventListener and using an EventFilter allows assertions on log messages, including those which are generated by exceptions:

  1. import akka.testkit.EventFilter
  2. import com.typesafe.config.ConfigFactory
  3.  
  4. implicit val system = ActorSystem("testsystem", ConfigFactory.parseString("""
  5. akka.loggers = ["akka.testkit.TestEventListener"]
  6. """))
  7. try {
  8. val actor = system.actorOf(Props.empty)
  9. EventFilter[ActorKilledException](occurrences = 1) intercept {
  10. actor ! Kill
  11. }
  12. } finally {
  13. shutdown(system)
  14. }

If a number of occurrences is specific—as demonstrated above—then intercept will block until that number of matching messages have been received or the timeout configured in akka.test.filter-leeway is used up (time starts counting after the passed-in block of code returns). In case of a timeout the test fails.

Note

Be sure to exchange the default logger with the TestEventListener in your application.conf to enable this function:

  1. akka.loggers = [akka.testkit.TestEventListener]

Timing Assertions

Another important part of functional testing concerns timing: certain events must not happen immediately (like a timer), others need to happen before a deadline. Therefore, all examination methods accept an upper time limit within the positive or negative result must be obtained. Lower time limits need to be checked external to the examination, which is facilitated by a new construct for managing time constraints:

  1. within([min, ]max) {
  2. ...
  3. }

The block given to within must complete after a Duration which is between min and max, where the former defaults to zero. The deadline calculated by adding the max parameter to the block's start time is implicitly available within the block to all examination methods, if you do not specify it, it is inherited from the innermost enclosingwithin block.

It should be noted that if the last message-receiving assertion of the block is expectNoMsg or receiveWhile, the final check of the within is skipped in order to avoid false positives due to wake-up latencies. This means that while individual contained assertions still use the maximum time bound, the overall block may take arbitrarily longer in this case.

  1. import akka.actor.Props
  2. import scala.concurrent.duration._
  3.  
  4. val worker = system.actorOf(Props[Worker])
  5. within(200 millis) {
  6. worker ! "some work"
  7. expectMsg("some result")
  8. expectNoMsg // will block for the rest of the 200ms
  9. Thread.sleep(300) // will NOT make this block fail
  10. }

Note

All times are measured using System.nanoTime, meaning that they describe wall time, not CPU time.

Ray Roestenburg has written a great article on using the TestKit: http://roestenburg.agilesquad.com/2011/02/unit-testing-akka-actors-with-testkit_12.html. His full example is also available here.

Accounting for Slow Test Systems

The tight timeouts you use during testing on your lightning-fast notebook will invariably lead to spurious test failures on the heavily loaded Jenkins server (or similar). To account for this situation, all maximum durations are internally scaled by a factor taken from the Configuration, akka.test.timefactor, which defaults to 1.

You can scale other durations with the same factor by using the implicit conversion in akka.testkit package object to add dilated function to Duration.

  1. import scala.concurrent.duration._
  2. import akka.testkit._
  3. 10.milliseconds.dilated

Resolving Conflicts with Implicit ActorRef

If you want the sender of messages inside your TestKit-based tests to be the testActor simply mix inImplicitSender into your test.

  1. class MySpec() extends TestKit(ActorSystem("MySpec")) with ImplicitSender
  2. with WordSpecLike with Matchers with BeforeAndAfterAll {

Using Multiple Probe Actors

When the actors under test are supposed to send various messages to different destinations, it may be difficult distinguishing the message streams arriving at the testActor when using the TestKit as a mixin. Another approach is to use it for creation of simple probe actors to be inserted in the message flows. To make this more powerful and convenient, there is a concrete implementation called TestProbe. The functionality is best explained using a small example:

  1. import scala.concurrent.duration._
  2. import scala.concurrent.Future
  3. import akka.actor._
  4. import akka.testkit.TestProbe
  1. class MyDoubleEcho extends Actor {
  2. var dest1: ActorRef = _
  3. var dest2: ActorRef = _
  4. def receive = {
  5. case (d1: ActorRef, d2: ActorRef) =>
  6. dest1 = d1
  7. dest2 = d2
  8. case x =>
  9. dest1 ! x
  10. dest2 ! x
  11. }
  12. }
  1. val probe1 = TestProbe()
  2. val probe2 = TestProbe()
  3. val actor = system.actorOf(Props[MyDoubleEcho])
  4. actor ! ((probe1.ref, probe2.ref))
  5. actor ! "hello"
  6. probe1.expectMsg(500 millis, "hello")
  7. probe2.expectMsg(500 millis, "hello")

Here a the system under test is simulated by MyDoubleEcho, which is supposed to mirror its input to two outputs. Attaching two test probes enables verification of the (simplistic) behavior. Another example would be two actors A and B which collaborate by A sending messages to B. In order to verify this message flow, a TestProbe could be inserted as target of A, using the forwarding capabilities or auto-pilot described below to include a real B in the test setup.

If you have many test probes, you can name them to get meaningful actor names in test logs and assertions:

  1. val worker = TestProbe("worker")
  2. val aggregator = TestProbe("aggregator")
  3.  
  4. worker.ref.path.name should startWith("worker")
  5. aggregator.ref.path.name should startWith("aggregator")

Probes may also be equipped with custom assertions to make your test code even more concise and clear:

  1. final case class Update(id: Int, value: String)
  2.  
  3. val probe = new TestProbe(system) {
  4. def expectUpdate(x: Int) = {
  5. expectMsgPF() {
  6. case Update(id, _) if id == x => true
  7. }
  8. sender() ! "ACK"
  9. }
  10. }

You have complete flexibility here in mixing and matching the TestKit facilities with your own checks and choosing an intuitive name for it. In real life your code will probably be a bit more complicated than the example given above; just use the power!

Warning

Any message send from a TestProbe to another actor which runs on the CallingThreadDispatcher runs the risk of dead-lock, if that other actor might also send to this probe. The implementation of TestProbe.watch andTestProbe.unwatch will also send a message to the watchee, which means that it is dangerous to try watching e.g. TestActorRef from a TestProbe.

Watching Other Actors from Probes

A TestProbe can register itself for DeathWatch of any other actor:

  1. val probe = TestProbe()
  2. probe watch target
  3. target ! PoisonPill
  4. probe.expectTerminated(target)

Replying to Messages Received by Probes

The probes keep track of the communications channel for replies, if possible, so they can also reply:

  1. val probe = TestProbe()
  2. val future = probe.ref ? "hello"
  3. probe.expectMsg(0 millis, "hello") // TestActor runs on CallingThreadDispatcher
  4. probe.reply("world")
  5. assert(future.isCompleted && future.value == Some(Success("world")))

Forwarding Messages Received by Probes

Given a destination actor dest which in the nominal actor network would receive a message from actor source. If you arrange for the message to be sent to a TestProbe probe instead, you can make assertions concerning volume and timing of the message flow while still keeping the network functioning:

  1. class Source(target: ActorRef) extends Actor {
  2. def receive = {
  3. case "start" => target ! "work"
  4. }
  5. }
  6.  
  7. class Destination extends Actor {
  8. def receive = {
  9. case x => // Do something..
  10. }
  11. }
  1. val probe = TestProbe()
  2. val source = system.actorOf(Props(classOf[Source], probe.ref))
  3. val dest = system.actorOf(Props[Destination])
  4. source ! "start"
  5. probe.expectMsg("work")
  6. probe.forward(dest)

The dest actor will receive the same message invocation as if no test probe had intervened.

Auto-Pilot

Receiving messages in a queue for later inspection is nice, but in order to keep a test running and verify traces later you can also install an AutoPilot in the participating test probes (actually in any TestKit) which is invoked before enqueueing to the inspection queue. This code can be used to forward messages, e.g. in a chain A --> Probe --> B, as long as a certain protocol is obeyed.

  1. val probe = TestProbe()
  2. probe.setAutoPilot(new TestActor.AutoPilot {
  3. def run(sender: ActorRef, msg: Any): TestActor.AutoPilot =
  4. msg match {
  5. case "stop" TestActor.NoAutoPilot
  6. case x testActor.tell(x, sender); TestActor.KeepRunning
  7. }
  8. })

The run method must return the auto-pilot for the next message, which may be KeepRunning to retain the current one or NoAutoPilot to switch it off.

Caution about Timing Assertions

The behavior of within blocks when using test probes might be perceived as counter-intuitive: you need to remember that the nicely scoped deadline as described above is local to each probe. Hence, probes do not react to each other's deadlines or to the deadline set in an enclosing TestKit instance:

  1. val probe = TestProbe()
  2. within(1 second) {
  3. probe.expectMsg("hello")
  4. }

Here, the expectMsg call will use the default timeout.

Testing parent-child relationships

The parent of an actor is always the actor that created it. At times this leads to a coupling between the two that may not be straightforward to test. Broadly, there are three approaches to improve testability of parent-child relationships:

  1. when creating a child, pass an explicit reference to its parent
  2. when creating a parent, tell the parent how to create its child
  3. create a fabricated parent when testing

For example, the structure of the code you want to test may follow this pattern:

  1. class Parent extends Actor {
  2. val child = context.actorOf(Props[Child], "child")
  3. var ponged = false
  4.  
  5. def receive = {
  6. case "pingit" => child ! "ping"
  7. case "pong" => ponged = true
  8. }
  9. }
  10.  
  11. class Child extends Actor {
  12. def receive = {
  13. case "ping" => context.parent ! "pong"
  14. }
  15. }

Using dependency-injection

The first option is to avoid use of the context.parent function and create a child with a custom parent by passing an explicit reference to its parent instead.

  1. class DependentChild(parent: ActorRef) extends Actor {
  2. def receive = {
  3. case "ping" => parent ! "pong"
  4. }
  5. }

Alternatively, you can tell the parent how to create its child. There are two ways to do this: by giving it a Props object or by giving it a function which takes care of creating the child actor:

  1. class DependentParent(childProps: Props) extends Actor {
  2. val child = context.actorOf(childProps, "child")
  3. var ponged = false
  4.  
  5. def receive = {
  6. case "pingit" => child ! "ping"
  7. case "pong" => ponged = true
  8. }
  9. }
  10.  
  11. class GenericDependentParent(childMaker: ActorRefFactory => ActorRef) extends Actor {
  12. val child = childMaker(context)
  13. var ponged = false
  14.  
  15. def receive = {
  16. case "pingit" => child ! "ping"
  17. case "pong" => ponged = true
  18. }
  19. }

Creating the Props is straightforward and the function may look like this in your test code:

  1. val maker = (_: ActorRefFactory) => probe.ref
  2. val parent = system.actorOf(Props(classOf[GenericDependentParent], maker))

And like this in your application code:

  1. val maker = (f: ActorRefFactory) => f.actorOf(Props[Child])
  2. val parent = system.actorOf(Props(classOf[GenericDependentParent], maker))

Using a fabricated parent

If you prefer to avoid modifying the parent or child constructor you can create a fabricated parent in your test. This, however, does not enable you to test the parent actor in isolation.

  1. "A fabricated parent" should {
  2. "test its child responses" in {
  3. val proxy = TestProbe()
  4. val parent = system.actorOf(Props(new Actor {
  5. val child = context.actorOf(Props[Child], "child")
  6. def receive = {
  7. case x if sender == child => proxy.ref forward x
  8. case x => child forward x
  9. }
  10. }))
  11.  
  12. proxy.send(parent, "ping")
  13. proxy.expectMsg("pong")
  14. }
  15. }

Which of these methods is the best depends on what is most important to test. The most generic option is to create the parent actor by passing it a function that is responsible for the Actor creation, but the fabricated parent is often sufficient.

CallingThreadDispatcher

The CallingThreadDispatcher serves good purposes in unit testing, as described above, but originally it was conceived in order to allow contiguous stack traces to be generated in case of an error. As this special dispatcher runs everything which would normally be queued directly on the current thread, the full history of a message's processing chain is recorded on the call stack, so long as all intervening actors run on this dispatcher.

How to use it

Just set the dispatcher as you normally would:

  1. import akka.testkit.CallingThreadDispatcher
  2. val ref = system.actorOf(Props[MyActor].withDispatcher(CallingThreadDispatcher.Id))

How it works

When receiving an invocation, the CallingThreadDispatcher checks whether the receiving actor is already active on the current thread. The simplest example for this situation is an actor which sends a message to itself. In this case, processing cannot continue immediately as that would violate the actor model, so the invocation is queued and will be processed when the active invocation on that actor finishes its processing; thus, it will be processed on the calling thread, but simply after the actor finishes its previous work. In the other case, the invocation is simply processed immediately on the current thread. Futures scheduled via this dispatcher are also executed immediately.

This scheme makes the CallingThreadDispatcher work like a general purpose dispatcher for any actors which never block on external events.

In the presence of multiple threads it may happen that two invocations of an actor running on this dispatcher happen on two different threads at the same time. In this case, both will be processed directly on their respective threads, where both compete for the actor's lock and the loser has to wait. Thus, the actor model is left intact, but the price is loss of concurrency due to limited scheduling. In a sense this is equivalent to traditional mutex style concurrency.

The other remaining difficulty is correct handling of suspend and resume: when an actor is suspended, subsequent invocations will be queued in thread-local queues (the same ones used for queuing in the normal case). The call toresume, however, is done by one specific thread, and all other threads in the system will probably not be executing this specific actor, which leads to the problem that the thread-local queues cannot be emptied by their native threads. Hence, the thread calling resume will collect all currently queued invocations from all threads into its own queue and process them.

Limitations

Warning

In case the CallingThreadDispatcher is used for top-level actors, but without going through TestActorRef, then there is a time window during which the actor is awaiting construction by the user guardian actor. Sending messages to the actor during this time period will result in them being enqueued and then executed on the guardian’s thread instead of the caller’s thread. To avoid this, use TestActorRef.

If an actor's behavior blocks on a something which would normally be affected by the calling actor after having sent the message, this will obviously dead-lock when using this dispatcher. This is a common scenario in actor tests based onCountDownLatch for synchronization:

  1. val latch = new CountDownLatch(1)
  2. actor ! startWorkAfter(latch) // actor will call latch.await() before proceeding
  3. doSomeSetupStuff()
  4. latch.countDown()

The example would hang indefinitely within the message processing initiated on the second line and never reach the fourth line, which would unblock it on a normal dispatcher.

Thus, keep in mind that the CallingThreadDispatcher is not a general-purpose replacement for the normal dispatchers. On the other hand it may be quite useful to run your actor network on it for testing, because if it runs without dead-locking chances are very high that it will not dead-lock in production.

Warning

The above sentence is unfortunately not a strong guarantee, because your code might directly or indirectly change its behavior when running on a different dispatcher. If you are looking for a tool to help you debug dead-locks, theCallingThreadDispatcher may help with certain error scenarios, but keep in mind that it has may give false negatives as well as false positives.

Thread Interruptions

If the CallingThreadDispatcher sees that the current thread has its isInterrupted() flag set when message processing returns, it will throw an InterruptedException after finishing all its processing (i.e. all messages which need processing as described above are processed before this happens). As tell cannot throw exceptions due to its contract, this exception will then be caught and logged, and the thread’s interrupted status will be set again.

If during message processing an InterruptedException is thrown then it will be caught inside the CallingThreadDispatcher’s message handling loop, the thread’s interrupted flag will be set and processing continues normally.

Note

The summary of these two paragraphs is that if the current thread is interrupted while doing work under the CallingThreadDispatcher, then that will result in the isInterrupted flag to be true when the message send returns and no InterruptedException will be thrown.

Benefits

To summarize, these are the features with the CallingThreadDispatcher has to offer:

  • Deterministic execution of single-threaded tests while retaining nearly full actor semantics
  • Full message processing history leading up to the point of failure in exception stack traces
  • Exclusion of certain classes of dead-lock scenarios

Tracing Actor Invocations

The testing facilities described up to this point were aiming at formulating assertions about a system’s behavior. If a test fails, it is usually your job to find the cause, fix it and verify the test again. This process is supported by debuggers as well as logging, where the Akka toolkit offers the following options:

  1. import akka.event.LoggingReceive
  2. def receive = LoggingReceive {
  3. case msg => // Do something ...
  4. }
  5. def otherState: Receive = LoggingReceive.withLabel("other") {
  6. case msg => // Do something else ...
  7. }

If the aforementioned setting is not given in the Configuration, this method will pass through the given Receivefunction unmodified, meaning that there is no runtime cost unless actually enabled.

The logging feature is coupled to this specific local mark-up because enabling it uniformly on all actors is not usually what you need, and it would lead to endless loops if it were applied to event bus logger listeners.

All these messages are logged at DEBUG level. To summarize, you can enable full logging of actor activities using this configuration fragment:

  1. akka {
  2. loglevel = "DEBUG"
  3. actor {
  4. debug {
  5. receive = on
  6. autoreceive = on
  7. lifecycle = on
  8. }
  9. }
  10. }

Different Testing Frameworks

Akka’s own test suite is written using ScalaTest, which also shines through in documentation examples. However, the TestKit and its facilities do not depend on that framework, you can essentially use whichever suits your development style best.

This section contains a collection of known gotchas with some other frameworks, which is by no means exhaustive and does not imply endorsement or special support.

When you need it to be a trait

If for some reason it is a problem to inherit from TestKit due to it being a concrete class instead of a trait, there’sTestKitBase:

  1. import akka.testkit.TestKitBase
  2.  
  3. class MyTest extends TestKitBase {
  4. implicit lazy val system = ActorSystem()
  5.  
  6. // put your test code here ...
  7.  
  8. shutdown(system)
  9. }

The implicit lazy val system must be declared exactly like that (you can of course pass arguments to the actor system factory as needed) because trait TestKitBase needs the system during its construction.

Warning

Use of the trait is discouraged because of potential issues with binary backwards compatibility in the future, use at own risk.

Specs2

Some Specs2 users have contributed examples of how to work around some clashes which may arise:

Configuration

There are several configuration properties for the TestKit module, please refer to the reference configuration.

Actor DSL

The Actor DSL

Simple actors—for example one-off workers or even when trying things out in the REPL—can be created more concisely using the Act trait. The supporting infrastructure is bundled in the following import:

  1. import akka.actor.ActorDSL._
  2. import akka.actor.ActorSystem
  3.  
  4. implicit val system = ActorSystem("demo")

This import is assumed for all code samples throughout this section. The implicit actor system serves asActorRefFactory for all examples below. To define a simple actor, the following is sufficient:

  1. val a = actor(new Act {
  2. become {
  3. case "hello" sender() ! "hi"
  4. }
  5. })

Here, actor takes the role of either system.actorOf or context.actorOf, depending on which context it is called in: it takes an implicit ActorRefFactory, which within an actor is available in the form of theimplicit val context: ActorContext. Outside of an actor, you’ll have to either declare an implicit ActorSystem, or you can give the factory explicitly (see further below).

The two possible ways of issuing a context.become (replacing or adding the new behavior) are offered separately to enable a clutter-free notation of nested receives:

  1. val a = actor(new Act {
  2. become { // this will replace the initial (empty) behavior
  3. case "info" sender() ! "A"
  4. case "switch"
  5. becomeStacked { // this will stack upon the "A" behavior
  6. case "info" sender() ! "B"
  7. case "switch" unbecome() // return to the "A" behavior
  8. }
  9. case "lobotomize" unbecome() // OH NOES: Actor.emptyBehavior
  10. }
  11. })

Please note that calling unbecome more often than becomeStacked results in the original behavior being installed, which in case of the Act trait is the empty behavior (the outer become just replaces it during construction).

Life-cycle management

Life-cycle hooks are also exposed as DSL elements (see Start Hook and Stop Hook), where later invocations of the methods shown below will replace the contents of the respective hooks:

  1. val a = actor(new Act {
  2. whenStarting { testActor ! "started" }
  3. whenStopping { testActor ! "stopped" }
  4. })

The above is enough if the logical life-cycle of the actor matches the restart cycles (i.e. whenStopping is executed before a restart and whenStarting afterwards). If that is not desired, use the following two hooks (see Restart Hooks):

  1. val a = actor(new Act {
  2. become {
  3. case "die" throw new Exception
  4. }
  5. whenFailing { case m @ (cause, msg) testActor ! m }
  6. whenRestarted { cause testActor ! cause }
  7. })

It is also possible to create nested actors, i.e. grand-children, like this:

  1. // here we pass in the ActorRefFactory explicitly as an example
  2. val a = actor(system, "fred")(new Act {
  3. val b = actor("barney")(new Act {
  4. whenStarting { context.parent ! ("hello from " + self.path) }
  5. })
  6. become {
  7. case x testActor ! x
  8. }
  9. })

Note

In some cases it will be necessary to explicitly pass the ActorRefFactory to the actor method (you will notice when the compiler tells you about ambiguous implicits).

The grand-child will be supervised by the child; the supervisor strategy for this relationship can also be configured using a DSL element (supervision directives are part of the Act trait):

  1. superviseWith(OneForOneStrategy() {
  2. case e: Exception if e.getMessage == "hello" Stop
  3. case _: Exception Resume
  4. })

Actor with Stash

Last but not least there is a little bit of convenience magic built-in, which detects if the runtime class of the statically given actor subtype extends the RequiresMessageQueue trait via the Stash trait (this is a complicated way of saying that new Act with Stash would not work because its runtime erased type is just an anonymous subtype of Act). The purpose is to automatically use the appropriate deque-based mailbox type required by Stash. If you want to use this magic, simply extend ActWithStash:

  1. val a = actor(new ActWithStash {
  2. become {
  3. case 1 stash()
  4. case 2
  5. testActor ! 2; unstashAll(); becomeStacked {
  6. case 1 testActor ! 1; unbecome()
  7. }
  8. }
  9. })

Типізовані актори

Note

This module will be deprecated as it will be superseded by the Akka Typed project which is currently being developed in open preview mode.

Akka Typed Actors is an implementation of the Active Objects pattern. Essentially turning method invocations into asynchronous dispatch instead of synchronous that has been the default way since Smalltalk came out.

Typed Actors consist of 2 "parts", a public interface and an implementation, and if you've done any work in "enterprise" Java, this will be very familiar to you. As with normal Actors you have an external API (the public interface instance) that will delegate method calls asynchronously to a private instance of the implementation.

The advantage of Typed Actors vs. Actors is that with TypedActors you have a static contract, and don't need to define your own messages, the downside is that it places some limitations on what you can do and what you can't, i.e. you cannot use become/unbecome.

Typed Actors are implemented using JDK Proxies which provide a pretty easy-worked API to intercept method calls.

Note

Just as with regular Akka Actors, Typed Actors process one call at a time.

When to use Typed Actors

Typed actors are nice for bridging between actor systems (the “inside”) and non-actor code (the “outside”), because they allow you to write normal OO-looking code on the outside. Think of them like doors: their practicality lies in interfacing between private sphere and the public, but you don’t want that many doors inside your house, do you? For a longer discussion see this blog post.

A bit more background: TypedActors can easily be abused as RPC, and that is an abstraction which is well-known to be leaky. Hence TypedActors are not what we think of first when we talk about making highly scalable concurrent software easier to write correctly. They have their niche, use them sparingly.

The tools of the trade

Before we create our first Typed Actor we should first go through the tools that we have at our disposal, it's located inakka.actor.TypedActor.

  1. import akka.actor.TypedActor
  2.  
  3. //Returns the Typed Actor Extension
  4. val extension = TypedActor(system) //system is an instance of ActorSystem
  5.  
  6. //Returns whether the reference is a Typed Actor Proxy or not
  7. TypedActor(system).isTypedActor(someReference)
  8.  
  9. //Returns the backing Akka Actor behind an external Typed Actor Proxy
  10. TypedActor(system).getActorRefFor(someReference)
  11.  
  12. //Returns the current ActorContext,
  13. // method only valid within methods of a TypedActor implementation
  14. val c: ActorContext = TypedActor.context
  15.  
  16. //Returns the external proxy of the current Typed Actor,
  17. // method only valid within methods of a TypedActor implementation
  18. val s: Squarer = TypedActor.self[Squarer]
  19.  
  20. //Returns a contextual instance of the Typed Actor Extension
  21. //this means that if you create other Typed Actors with this,
  22. //they will become children to the current Typed Actor.
  23. TypedActor(TypedActor.context)

Warning

Same as not exposing this of an Akka Actor, it's important not to expose this of a Typed Actor, instead you should pass the external proxy reference, which is obtained from within your Typed Actor as TypedActor.self, this is your external identity, as the ActorRef is the external identity of an Akka Actor.

Creating Typed Actors

To create a Typed Actor you need to have one or more interfaces, and one implementation.

Our example interface:

  1. trait Squarer {
  2. def squareDontCare(i: Int): Unit //fire-forget
  3.  
  4. def square(i: Int): Future[Int] //non-blocking send-request-reply
  5.  
  6. def squareNowPlease(i: Int): Option[Int] //blocking send-request-reply
  7.  
  8. def squareNow(i: Int): Int //blocking send-request-reply
  9.  
  10. @throws(classOf[Exception]) //declare it or you will get an UndeclaredThrowableException
  11. def squareTry(i: Int): Int //blocking send-request-reply with possible exception
  12. }

Alright, now we've got some methods we can call, but we need to implement those in SquarerImpl.

  1. class SquarerImpl(val name: String) extends Squarer {
  2.  
  3. def this() = this("default")
  4. def squareDontCare(i: Int): Unit = i * i //Nobody cares :(
  5.  
  6. def square(i: Int): Future[Int] = Future.successful(i * i)
  7.  
  8. def squareNowPlease(i: Int): Option[Int] = Some(i * i)
  9.  
  10. def squareNow(i: Int): Int = i * i
  11.  
  12. def squareTry(i: Int): Int = throw new Exception("Catch me!")
  13. }

Excellent, now we have an interface and an implementation of that interface, and we know how to create a Typed Actor from that, so let's look at calling these methods.

The most trivial way of creating a Typed Actor instance of our Squarer:

  1. val mySquarer: Squarer =
  2. TypedActor(system).typedActorOf(TypedProps[SquarerImpl]())

First type is the type of the proxy, the second type is the type of the implementation. If you need to call a specific constructor you do it like this:

  1. val otherSquarer: Squarer =
  2. TypedActor(system).typedActorOf(TypedProps(
  3. classOf[Squarer],
  4. new SquarerImpl("foo")), "name")

Since you supply a Props, you can specify which dispatcher to use, what the default timeout should be used and more.

Method dispatch semantics

Methods returning:

  • Unit will be dispatched with fire-and-forget semantics, exactly like ActorRef.tell
  • scala.concurrent.Future[_] will use send-request-reply semantics, exactly like ActorRef.ask
  • scala.Option[_] will use send-request-reply semantics, but will block to wait for an answer, and returnscala.None if no answer was produced within the timeout, or scala.Some[_] containing the result otherwise. Any exception that was thrown during this call will be rethrown.
  • Any other type of value will use send-request-reply semantics, but will block to wait for an answer, throwingjava.util.concurrent.TimeoutException if there was a timeout or rethrow any exception that was thrown during this call.

Messages and immutability

While Akka cannot enforce that the parameters to the methods of your Typed Actors are immutable, we stronglyrecommend that parameters passed are immutable.

One-way message send

  1. mySquarer.squareDontCare(10)

As simple as that! The method will be executed on another thread; asynchronously.

Request-reply message send

  1. val oSquare = mySquarer.squareNowPlease(10) //Option[Int]

This will block for as long as the timeout that was set in the Props of the Typed Actor, if needed. It will return None if a timeout occurs.

  1. val iSquare = mySquarer.squareNow(10) //Int

This will block for as long as the timeout that was set in the Props of the Typed Actor, if needed. It will throw ajava.util.concurrent.TimeoutException if a timeout occurs.

Request-reply-with-future message send

  1. val fSquare = mySquarer.square(10) //A Future[Int]

This call is asynchronous, and the Future returned can be used for asynchronous composition.

Stopping Typed Actors

Since Akka's Typed Actors are backed by Akka Actors they must be stopped when they aren't needed anymore.

  1. TypedActor(system).stop(mySquarer)

This asynchronously stops the Typed Actor associated with the specified proxy ASAP.

  1. TypedActor(system).poisonPill(otherSquarer)

This asynchronously stops the Typed Actor associated with the specified proxy after it's done with all calls that were made prior to this call.

Typed Actor Hierarchies

Since you can obtain a contextual Typed Actor Extension by passing in an ActorContext you can create child Typed Actors by invoking typedActorOf(..) on that:

  1. //Inside your Typed Actor
  2. val childSquarer: Squarer =
  3. TypedActor(TypedActor.context).typedActorOf(TypedProps[SquarerImpl]())
  4. //Use "childSquarer" as a Squarer

You can also create a child Typed Actor in regular Akka Actors by giving the ActorContext as an input parameter to TypedActor.get(…).

Supervisor Strategy

By having your Typed Actor implementation class implement TypedActor.Supervisor you can define the strategy to use for supervising child actors, as described in Supervision and Monitoring and Fault Tolerance.

Lifecycle callbacks

By having your Typed Actor implementation class implement any and all of the following:

  • TypedActor.PreStart
  • TypedActor.PostStop
  • TypedActor.PreRestart
  • TypedActor.PostRestart

You can hook into the lifecycle of your Typed Actor.

Receive arbitrary messages

If your implementation class of your TypedActor extends akka.actor.TypedActor.Receiver, all messages that are not MethodCall instances will be passed into the onReceive-method.

This allows you to react to DeathWatch Terminated-messages and other types of messages, e.g. when interfacing with untyped actors.

Proxying

You can use the typedActorOf that takes a TypedProps and an ActorRef to proxy the given ActorRef as a TypedActor. This is usable if you want to communicate remotely with TypedActors on other machines, just pass the ActorRef totypedActorOf.

Note

The ActorRef needs to accept MethodCall messages.

Lookup & Remoting

Since TypedActors are backed by Akka Actors, you can use typedActorOf to proxy ActorRefs potentially residing on remote nodes.

  1. val typedActor: Foo with Bar =
  2. TypedActor(system).
  3. typedActorOf(
  4. TypedProps[FooBar],
  5. actorRefToRemoteActor)
  6. //Use "typedActor" as a FooBar

Supercharging

Here's an example on how you can use traits to mix in behavior in your Typed Actors.

  1. trait Foo {
  2. def doFoo(times: Int): Unit = println("doFoo(" + times + ")")
  3. }
  4.  
  5. trait Bar {
  6. def doBar(str: String): Future[String] =
  7. Future.successful(str.toUpperCase)
  8. }
  9.  
  10. class FooBar extends Foo with Bar
  1. val awesomeFooBar: Foo with Bar =
  2. TypedActor(system).typedActorOf(TypedProps[FooBar]())
  3.  
  4. awesomeFooBar.doFoo(10)
  5. val f = awesomeFooBar.doBar("yes")
  6.  
  7. TypedActor(system).poisonPill(awesomeFooBar)

Typed Router pattern

Sometimes you want to spread messages between multiple actors. The easiest way to achieve this in Akka is to use aRouter, which can implement a specific routing logic, such as smallest-mailbox or consistent-hashing etc.

Routers are not provided directly for typed actors, but it is really easy to leverage an untyped router and use a typed proxy in front of it. To showcase this let's create typed actors that assign themselves some random id, so we know that in fact, the router has sent the message to different actors:

  1. trait HasName {
  2. def name(): String
  3. }
  4.  
  5. class Named extends HasName {
  6. import scala.util.Random
  7. private val id = Random.nextInt(1024)
  8.  
  9. def name(): String = "name-" + id
  10. }

In order to round robin among a few instances of such actors, you can simply create a plain untyped router, and then facade it with a TypedActor like shown in the example below. This works because typed actors of course communicate using the same mechanisms as normal actors, and methods calls on them get transformed into message sends of MethodCall messages.

  1. def namedActor(): HasName = TypedActor(system).typedActorOf(TypedProps[Named]())
  2.  
  3. // prepare routees
  4. val routees: List[HasName] = List.fill(5) { namedActor() }
  5. val routeePaths = routees map { r =>
  6. TypedActor(system).getActorRefFor(r).path.toStringWithoutAddress
  7. }
  8.  
  9. // prepare untyped router
  10. val router: ActorRef = system.actorOf(RoundRobinGroup(routeePaths).props())
  11.  
  12. // prepare typed proxy, forwarding MethodCall messages to `router`
  13. val typedRouter: HasName =
  14. TypedActor(system).typedActorOf(TypedProps[Named](), actorRef = router)
  15.  
  16. println("actor was: " + typedRouter.name()) // name-184
  17. println("actor was: " + typedRouter.name()) // name-753
  18. println("actor was: " + typedRouter.name()) // name-320
  19. println("actor was: " + typedRouter.name()) // name-164

Futures

Introduction

In the Scala Standard Library, a Future is a data structure used to retrieve the result of some concurrent operation. This result can be accessed synchronously (blocking) or asynchronously (non-blocking).

Execution Contexts

In order to execute callbacks and operations, Futures need something called an ExecutionContext, which is very similar to a java.util.concurrent.Executor. if you have an ActorSystem in scope, it will use its default dispatcher as the ExecutionContext, or you can use the factory methods provided by the ExecutionContextcompanion object to wrap Executors and ExecutorServices, or even create your own.

  1. import scala.concurrent.{ ExecutionContext, Promise }
  2.  
  3. implicit val ec = ExecutionContext.fromExecutorService(yourExecutorServiceGoesHere)
  4.  
  5. // Do stuff with your brand new shiny ExecutionContext
  6. val f = Promise.successful("foo")
  7.  
  8. // Then shut your ExecutionContext down at some
  9. // appropriate place in your program/application
  10. ec.shutdown()

Within Actors

Each actor is configured to be run on a MessageDispatcher, and that dispatcher doubles as an ExecutionContext. If the nature of the Future calls invoked by the actor matches or is compatible with the activities of that actor (e.g. all CPU bound and no latency requirements), then it may be easiest to reuse the dispatcher for running the Futures by importing context.dispatcher.

  1. class A extends Actor {
  2. import context.dispatcher
  3. val f = Future("hello")
  4. def receive = {
  5. // receive omitted ...
  6. }
  7. }

Use With Actors

There are generally two ways of getting a reply from an Actor: the first is by a sent message (actor ! msg), which only works if the original sender was an Actor) and the second is through a Future.

Using an Actor's ? method to send a message will return a Future:

  1. import scala.concurrent.Await
  2. import akka.pattern.ask
  3. import akka.util.Timeout
  4. import scala.concurrent.duration._
  5.  
  6. implicit val timeout = Timeout(5 seconds)
  7. val future = actor ? msg // enabled by the “ask” import
  8. val result = Await.result(future, timeout.duration).asInstanceOf[String]

This will cause the current thread to block and wait for the Actor to 'complete' the Future with it's reply. Blocking is discouraged though as it will cause performance problems. The blocking operations are located in Await.result andAwait.ready to make it easy to spot where blocking occurs. Alternatives to blocking are discussed further within this documentation. Also note that the Future returned by an Actor is a Future[Any] since an Actor is dynamic. That is why the asInstanceOf is used in the above sample. When using non-blocking it is better to use the mapTomethod to safely try to cast a Future to an expected type:

  1. import scala.concurrent.Future
  2. import akka.pattern.ask
  3.  
  4. val future: Future[String] = ask(actor, msg).mapTo[String]

The mapTo method will return a new Future that contains the result if the cast was successful, or aClassCastException if not. Handling Exceptions will be discussed further within this documentation.

To send the result of a Future to an Actor, you can use the pipe construct:

  1. import akka.pattern.pipe
  2. future pipeTo actor

Use Directly

A common use case within Akka is to have some computation performed concurrently without needing the extra utility of an Actor. If you find yourself creating a pool of Actors for the sole reason of performing a calculation in parallel, there is an easier (and faster) way:

  1. import scala.concurrent.Await
  2. import scala.concurrent.Future
  3. import scala.concurrent.duration._
  4.  
  5. val future = Future {
  6. "Hello" + "World"
  7. }
  8. future foreach println

In the above code the block passed to Future will be executed by the default Dispatcher, with the return value of the block used to complete the Future (in this case, the result would be the string: "HelloWorld"). Unlike a Futurethat is returned from an Actor, this Future is properly typed, and we also avoid the overhead of managing anActor.

You can also create already completed Futures using the Future companion, which can be either successes:

  1. val future = Future.successful("Yay!")

Or failures:

  1. val otherFuture = Future.failed[String](new IllegalArgumentException("Bang!"))

It is also possible to create an empty Promise, to be filled later, and obtain the corresponding Future:

  1. val promise = Promise[String]()
  2. val theFuture = promise.future
  3. promise.success("hello")

Functional Futures

Scala's Future has several monadic methods that are very similar to the ones used by Scala's collections. These allow you to create 'pipelines' or 'streams' that the result will travel through.

Future is a Monad

The first method for working with Future functionally is map. This method takes a Function which performs some operation on the result of the Future, and returning a new result. The return value of the map method is anotherFuture that will contain the new result:

  1. val f1 = Future {
  2. "Hello" + "World"
  3. }
  4. val f2 = f1 map { x =>
  5. x.length
  6. }
  7. f2 foreach println

In this example we are joining two strings together within a Future. Instead of waiting for this to complete, we apply our function that calculates the length of the string using the map method. Now we have a second Future that will eventually contain an Int. When our original Future completes, it will also apply our function and complete the second Future with its result. When we finally get the result, it will contain the number 10. Our original Future still contains the string "HelloWorld" and is unaffected by the map.

The map method is fine if we are modifying a single Future, but if 2 or more Futures are involved map will not allow you to combine them together:

  1. val f1 = Future {
  2. "Hello" + "World"
  3. }
  4. val f2 = Future.successful(3)
  5. val f3 = f1 map { x =>
  6. f2 map { y =>
  7. x.length * y
  8. }
  9. }
  10. f3 foreach println

f3 is a Future[Future[Int]] instead of the desired Future[Int]. Instead, the flatMap method should be used:

  1. val f1 = Future {
  2. "Hello" + "World"
  3. }
  4. val f2 = Future.successful(3)
  5. val f3 = f1 flatMap { x =>
  6. f2 map { y =>
  7. x.length * y
  8. }
  9. }
  10. f3 foreach println

Composing futures using nested combinators it can sometimes become quite complicated and hard to read, in these cases using Scala's 'for comprehensions' usually yields more readable code. See next section for examples.

If you need to do conditional propagation, you can use filter:

  1. val future1 = Future.successful(4)
  2. val future2 = future1.filter(_ % 2 == 0)
  3.  
  4. future2 foreach println
  5.  
  6. val failedFilter = future1.filter(_ % 2 == 1).recover {
  7. // When filter fails, it will have a java.util.NoSuchElementException
  8. case m: NoSuchElementException => 0
  9. }
  10.  
  11. failedFilter foreach println

For Comprehensions

Since Future has a map, filter and flatMap method it can be easily used in a 'for comprehension':

  1. val f = for {
  2. a <- Future(10 / 2) // 10 / 2 = 5
  3. b <- Future(a + 1) // 5 + 1 = 6
  4. c <- Future(a - 1) // 5 - 1 = 4
  5. if c > 3 // Future.filter
  6. } yield b * c // 6 * 4 = 24
  7.  
  8. // Note that the execution of futures a, b, and c
  9. // are not done in parallel.
  10.  
  11. f foreach println

Something to keep in mind when doing this is even though it looks like parts of the above example can run in parallel, each step of the for comprehension is run sequentially. This will happen on separate threads for each step but there isn't much benefit over running the calculations all within a single Future. The real benefit comes when the Futures are created first, and then combining them together.

Composing Futures

The example for comprehension above is an example of composing Futures. A common use case for this is combining the replies of several Actors into a single calculation without resorting to calling Await.result or Await.ready to block for each result. First an example of using Await.result:

  1. val f1 = ask(actor1, msg1)
  2. val f2 = ask(actor2, msg2)
  3.  
  4. val a = Await.result(f1, 3 seconds).asInstanceOf[Int]
  5. val b = Await.result(f2, 3 seconds).asInstanceOf[Int]
  6.  
  7. val f3 = ask(actor3, (a + b))
  8.  
  9. val result = Await.result(f3, 3 seconds).asInstanceOf[Int]

Warning

Await.result and Await.ready are provided for exceptional situations where you must block, a good rule of thumb is to only use them if you know why you must block. For all other cases, use asynchronous composition as described below.

Here we wait for the results from the first 2 Actors before sending that result to the third Actor. We calledAwait.result 3 times, which caused our little program to block 3 times before getting our final result. Now compare that to this example:

  1. val f1 = ask(actor1, msg1)
  2. val f2 = ask(actor2, msg2)
  3.  
  4. val f3 = for {
  5. a <- f1.mapTo[Int]
  6. b <- f2.mapTo[Int]
  7. c <- ask(actor3, (a + b)).mapTo[Int]
  8. } yield c
  9.  
  10. f3 foreach println

Here we have 2 actors processing a single message each. Once the 2 results are available (note that we don't block to get these results!), they are being added together and sent to a third Actor, which replies with a string, which we assign to 'result'.

This is fine when dealing with a known amount of Actors, but can grow unwieldy if we have more than a handful. Thesequence and traverse helper methods can make it easier to handle more complex use cases. Both of these methods are ways of turning, for a subclass T of Traversable, T[Future[A]] into a Future[T[A]]. For example:

  1. // oddActor returns odd numbers sequentially from 1 as a List[Future[Int]]
  2. val listOfFutures = List.fill(100)(akka.pattern.ask(oddActor, GetNext).mapTo[Int])
  3.  
  4. // now we have a Future[List[Int]]
  5. val futureList = Future.sequence(listOfFutures)
  6.  
  7. // Find the sum of the odd numbers
  8. val oddSum = futureList.map(_.sum)
  9. oddSum foreach println

To better explain what happened in the example, Future.sequence is taking the List[Future[Int]] and turning it into a Future[List[Int]]. We can then use map to work with the List[Int] directly, and we find the sum of theList.

The traverse method is similar to sequence, but it takes a T[A] and a function A => Future[B] to return aFuture[T[B]], where T is again a subclass of Traversable. For example, to use traverse to sum the first 100 odd numbers:

  1. val futureList = Future.traverse((1 to 100).toList)(x => Future(x * 2 - 1))
  2. val oddSum = futureList.map(_.sum)
  3. oddSum foreach println

This is the same result as this example:

  1. val futureList = Future.sequence((1 to 100).toList.map(x => Future(x * 2 - 1)))
  2. val oddSum = futureList.map(_.sum)
  3. oddSum foreach println

But it may be faster to use traverse as it doesn't have to create an intermediate List[Future[Int]].

Then there's a method that's called fold that takes a start-value, a sequence of Futures and a function from the type of the start-value and the type of the futures and returns something with the same type as the start-value, and then applies the function to all elements in the sequence of futures, asynchronously, the execution will start when the last of the Futures is completed.

  1. // Create a sequence of Futures
  2. val futures = for (i <- 1 to 1000) yield Future(i * 2)
  3. val futureSum = Future.fold(futures)(0)(_ + _)
  4. futureSum foreach println

That's all it takes!

If the sequence passed to fold is empty, it will return the start-value, in the case above, that will be 0. In some cases you don't have a start-value and you're able to use the value of the first completing Future in the sequence as the start-value, you can use reduce, it works like this:

  1. // Create a sequence of Futures
  2. val futures = for (i <- 1 to 1000) yield Future(i * 2)
  3. val futureSum = Future.reduce(futures)(_ + _)
  4. futureSum foreach println

Same as with fold, the execution will be done asynchronously when the last of the Future is completed, you can also parallelize it by chunking your futures into sub-sequences and reduce them, and then reduce the reduced results again.

Callbacks

Sometimes you just want to listen to a Future being completed, and react to that not by creating a new Future, but by side-effecting. For this Scala supports onComplete, onSuccess and onFailure, of which the latter two are specializations of the first.

  1. future onSuccess {
  2. case "bar" => println("Got my bar alright!")
  3. case x: String => println("Got some random string: " + x)
  4. }
  1. future onFailure {
  2. case ise: IllegalStateException if ise.getMessage == "OHNOES" =>
  3. //OHNOES! We are in deep trouble, do something!
  4. case e: Exception =>
  5. //Do something else
  6. }
  1. future onComplete {
  2. case Success(result) => doSomethingOnSuccess(result)
  3. case Failure(failure) => doSomethingOnFailure(failure)
  4. }

Define Ordering

Since callbacks are executed in any order and potentially in parallel, it can be tricky at the times when you need sequential ordering of operations. But there's a solution and it's name is andThen. It creates a new Future with the specified callback, a Future that will have the same result as the Future it's called on, which allows for ordering like in the following sample:

  1. val result = Future { loadPage(url) } andThen {
  2. case Failure(exception) => log(exception)
  3. } andThen {
  4. case _ => watchSomeTV()
  5. }
  6. result foreach println

Auxiliary Methods

Future fallbackTo combines 2 Futures into a new Future, and will hold the successful value of the secondFuture if the first Future fails.

  1. val future4 = future1 fallbackTo future2 fallbackTo future3
  2. future4 foreach println

You can also combine two Futures into a new Future that will hold a tuple of the two Futures successful results, using the zip operation.

  1. val future3 = future1 zip future2 map { case (a, b) => a + " " + b }
  2. future3 foreach println

Exceptions

Since the result of a Future is created concurrently to the rest of the program, exceptions must be handled differently. It doesn't matter if an Actor or the dispatcher is completing the Future, if an Exception is caught the Futurewill contain it instead of a valid result. If a Future does contain an Exception, calling Await.result will cause it to be thrown again so it can be handled properly.

It is also possible to handle an Exception by returning a different result. This is done with the recover method. For example:

  1. val future = akka.pattern.ask(actor, msg1) recover {
  2. case e: ArithmeticException => 0
  3. }
  4. future foreach println

In this example, if the actor replied with a akka.actor.Status.Failure containing the ArithmeticException, ourFuture would have a result of 0. The recover method works very similarly to the standard try/catch blocks, so multiple Exceptions can be handled in this manner, and if an Exception is not handled this way it will behave as if we hadn't used the recover method.

You can also use the recoverWith method, which has the same relationship to recover as flatMap has to map, and is use like this:

  1. val future = akka.pattern.ask(actor, msg1) recoverWith {
  2. case e: ArithmeticException => Future.successful(0)
  3. case foo: IllegalArgumentException =>
  4. Future.failed[Int](new IllegalStateException("All br0ken!"))
  5. }
  6. future foreach println

After

akka.pattern.after makes it easy to complete a Future with a value or exception after a timeout.

  1. // TODO after is unfortunately shadowed by ScalaTest, fix as part of #3759
  2. // import akka.pattern.after
  3.  
  4. val delayed = akka.pattern.after(200 millis, using = system.scheduler)(Future.failed(
  5. new IllegalStateException("OHNOES")))
  6. val future = Future { Thread.sleep(1000); "foo" }
  7. val result = Future firstCompletedOf Seq(future, delayed)

Агенти

Агенти в Akka надихались агентами в Clojure.

Агенти провадять асинхронні зміни власних розташуваннях. Агенти прив'язані до одного розташування зберігання на протязі всього життя, та їм дозволено змінювати тільки це розташування (на новий стан), що відбувається як result of an action. Update actions are functions that are asynchronously applied to the Agent's state and whose return value becomes the Agent's new state. The state of an Agent should be immutable.

While updates to Agents are asynchronous, the state of an Agent is always immediately available for reading by any thread (using get or apply) without any messages.

Agents are reactive. The update actions of all Agents get interleaved amongst threads in an ExecutionContext. At any point in time, at most one send action for each Agent is being executed. Actions dispatched to an agent from another thread will occur in the order they were sent, potentially interleaved with actions dispatched to the same agent from other threads.

Note

Agents are local to the node on which they are created. This implies that you should generally not include them in messages that may be passed to remote Actors or as constructor parameters for remote Actors; those remote Actors will not be able to read or update the Agent.

Creating Agents

Agents are created by invoking Agent(value) passing in the Agent's initial value and providing an implicitExecutionContext to be used for it, for these examples we're going to use the default global one, but YMMV:

  1. import scala.concurrent.ExecutionContext.Implicits.global
  2. import akka.agent.Agent
  3. val agent = Agent(5)

Reading an Agent's value

Agents can be dereferenced (you can get an Agent's value) by invoking the Agent with parentheses like this:

  1. val result = agent()

Or by using the get method:

  1. val result = agent.get

Reading an Agent's current value does not involve any message passing and happens immediately. So while updates to an Agent are asynchronous, reading the state of an Agent is synchronous.

Updating Agents (send & alter)

You update an Agent by sending a function that transforms the current value or by sending just a new value. The Agent will apply the new value or function atomically and asynchronously. The update is done in a fire-forget manner and you are only guaranteed that it will be applied. There is no guarantee of when the update will be applied but dispatches to an Agent from a single thread will occur in order. You apply a value or a function by invoking the send function.

  1. // send a value, enqueues this change
  2. // of the value of the Agent
  3. agent send 7
  4.  
  5. // send a function, enqueues this change
  6. // to the value of the Agent
  7. agent send (_ + 1)
  8. agent send (_ * 2)

You can also dispatch a function to update the internal state but on its own thread. This does not use the reactive thread pool and can be used for long-running or blocking operations. You do this with the sendOff method. Dispatches using either sendOff or send will still be executed in order.

  1. // the ExecutionContext you want to run the function on
  2. implicit val ec = someExecutionContext()
  3. // sendOff a function
  4. agent sendOff longRunningOrBlockingFunction

All send methods also have a corresponding alter method that returns a Future. See Futures for more information on Futures.

  1. // alter a value
  2. val f1: Future[Int] = agent alter 7
  3.  
  4. // alter a function
  5. val f2: Future[Int] = agent alter (_ + 1)
  6. val f3: Future[Int] = agent alter (_ * 2)
  1. // the ExecutionContext you want to run the function on
  2. implicit val ec = someExecutionContext()
  3. // alterOff a function
  4. val f4: Future[Int] = agent alterOff longRunningOrBlockingFunction

Awaiting an Agent's value

You can also get a Future to the Agents value, that will be completed after the currently queued updates have completed:

  1. val future = agent.future

See Futures for more information on Futures.

Monadic usage

Agents are also monadic, allowing you to compose operations using for-comprehensions. In monadic usage, new Agents are created leaving the original Agents untouched. So the old values (Agents) are still available as-is. They are so-called 'persistent'.

Example of monadic usage:

  1. import scala.concurrent.ExecutionContext.Implicits.global
  2. val agent1 = Agent(3)
  3. val agent2 = Agent(5)
  4.  
  5. // uses foreach
  6. for (value <- agent1)
  7. println(value)
  8.  
  9. // uses map
  10. val agent3 = for (value <- agent1) yield value + 1
  11.  
  12. // or using map directly
  13. val agent4 = agent1 map (_ + 1)
  14.  
  15. // uses flatMap
  16. val agent5 = for {
  17. value1 <- agent1
  18. value2 <- agent2
  19. } yield value1 + value2

Configuration

There are several configuration properties for the agents module, please refer to the reference configuration.

Deprecated Transactional Agents

Agents participating in enclosing STM transaction is a deprecated feature in 2.3.

If an Agent is used within an enclosing transaction, then it will participate in that transaction. If you send to an Agent within a transaction then the dispatch to the Agent will be held until that transaction commits, and discarded if the transaction is aborted. Here's an example:

  1. import scala.concurrent.ExecutionContext.Implicits.global
  2. import akka.agent.Agent
  3. import scala.concurrent.duration._
  4. import scala.concurrent.stm._
  5.  
  6. def transfer(from: Agent[Int], to: Agent[Int], amount: Int): Boolean = {
  7. atomic { txn =>
  8. if (from.get < amount) false
  9. else {
  10. from send (_ - amount)
  11. to send (_ + amount)
  12. true
  13. }
  14. }
  15. }
  16.  
  17. val from = Agent(100)
  18. val to = Agent(20)
  19. val ok = transfer(from, to, 50)
  20.  
  21. val fromValue = from.future // -> 50
  22. val toValue = to.future // -> 70

0

0

0

0